Зарегистрируйтесь, чтобы продолжить обучение

Работа с базой данныx в Slim Веб-разработка на PHP

Репозитории, которыми мы пользовались в уроках этого курса, хранят свои данные в сессии. Это было удобно для того, чтобы не отвлекаться на взаимодействие с базой и сфокусироваться на особенностях работы веба.

Теперь мы привыкли к фреймворку и понимаем принципы создания приложений на нем, поэтому можно начинать работу с реальной базой данных.

Чтобы начать хранить и извлекать данные из базы, нам нужно выполнить несколько действий:

  • Установить зависимости, необходимые для работы с базой данных
  • Настроить подключение к базе данных и дать к нему доступ из приложения
  • Создать начальную структуру базы данных с нужными таблицами
  • Переписать методы репозиториев так, чтобы они работали с данными через базу

В этом уроке мы проделаем все эти шаги на примере создания части CRUD для сущности Car с полями make (марка) и model (модель).

Устанавливаем зависимости

Для простоты мы будем использовать базу данных SQLite с хранением данных в файле. Этого достаточно для обучения, но в реальном окружении уже понадобится поставить PostgreSQL или его аналог.

Чтобы начать работать, вам потребуется установить PDO и драйвер SQLlite. Сделать это можно по нашей инструкции. Чтобы подключится к другой базе данных, нужно будет по аналогии установить соответствующий драйвер

Настраиваем подключение

Рассмотрим такой пример

<?php

$container = new Container();

$container->set(\PDO::class, function () {
    $conn = new \PDO('sqlite:database.sqlite');
    $conn->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
    return $conn;
});

// Регистрируем в контейнере другие зависимости

$app = AppFactory::createFromContainer($container);
// Остальной код

В примере выше мы создаем соединение с базой данных SQLlite с расположением в файле database.sqlite и регистрируем его в контейнере. Соединение потребуется нам при создании репозитория, так как вся работа с базой будет сосредоточена там.

Файл базы данных database.sqlite, если еще не существует, будет создан автоматически при первом обращении к базе. Поскольку этот файл содержит все данные приложения, его следует добавить в .gitignore, чтобы он не попал в git-репозиторий.

Строим начальную структуру базы данных

В нашем случае база данных создается при старте приложения, поэтому ее инициализацию мы будем делать там же, во время старта. Для этого создадим файл с нужной схемой данных и затем добавим ее в базу данных:

  1. Добавляем файл init.sql:

    CREATE TABLE IF NOT EXISTS cars (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        make VARCHAR(255) NOT NULL,
        model VARCHAR(255) NOT NULL
    );
    
  2. Загружаем схему в базу:

    <?php
    
    $container->set(\PDO::class, function () {
        $conn = new \PDO('sqlite:hexlet');
        $conn->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
        return $conn;
    });
    
    $initFilePath = implode('/', [dirname(__DIR__), 'init.sql']);
    $initSql = file_get_contents($initFilePath);
    $container->get(\PDO::class)->exec($initSql);
    
    $app = AppFactory::createFromContainer($container);
    

Создаем репозиторий CarRepository

В репозитории нам понадобится соединение с базой данных, так как вся работа с базой будет сосредоточена тут. Соединение будет передаваться в конструктор при создании объекта репозитория. Обратите внимание, что мы указали тип для параметра конструктора. Это нужно для того, чтобы контейнер внедрения зависимостей понимал, что нам нужен именно объект класса \PDO для создания объекта репозитория.

<?php

class CarRepository
{
    private \PDO $conn;

    public function __construct(\PDO $conn)
    {
        $this->conn = $conn;
    }

    public function getEntities(): array
    {
        $cars = [];
        $sql = "SELECT * FROM cars";
        $stmt = $this->conn->query($sql);

        while ($row = $stmt->fetch()) {
            $car = Car::fromArray([$row['make'], $row['model']]);
            $car->setId($row['id']);
            $cars[] = $car;
        }

        return $cars;
    }

    public function find(int $id): ?Car
    {
        $sql = "SELECT * FROM cars WHERE id = ?";
        $stmt = $this->conn->prepare($sql);
        $stmt->execute([$id]);
        if ($row = $stmt->fetch())  {
            $car = Car::fromArray([$row['make'], $row['model']]);
            $car->setId($row['id']);
            return $car;
        }

        return null;
    }

    public function save(Car $car): void {
        if ($car->exists()) {
            $this->update($car);
        } else {
            $this->create($car);
        }
    }

    private function update(Car $car): void
    {
        $sql = "UPDATE cars SET make = :make, model = :model WHERE id = :id";
        $stmt = $this->conn->prepare($sql);
        $id = $car->getId();
        $make = $car->getMake();
        $model = $car->getModel();
        $stmt->bindParam(':make', $make);
        $stmt->bindParam(':model', $model);
        $stmt->bindParam(':id', $id);
        $stmt->execute();
    }

    private function create(Car $car): void
    {
        $sql = "INSERT INTO cars (make, model) VALUES (:make, :model)";
        $stmt = $this->conn->prepare($sql);
        $make = $car->getMake();
        $model = $car->getModel();
        $stmt->bindParam(':make', $make);
        $stmt->bindParam(':model', $model);
        $stmt->execute();
        $id = (int) $this->conn->lastInsertId();
        $car->setId($id);
    }
}

Принцип создания всех методов для работы с базой данных одинаковый:

  • Описываем шаблон запроса
  • Формируем стейтмент
  • Делаем подстановки
  • Выполняем запрос
  • Собираем результат
  • Возвращаем ответ

Рассматриваем примеры операций

Структура контроллеров не меняется, несмотря на все изменения, которые мы сделали. Как видно на примере ниже, правильная организация абстракций и разделение по слоям приводят к тому, что изменение внутренностей не оказывает особого влияния на строение приложения:

<?php

$app->get('/cars', function ($request, $response) {
    $carRepository = $this->get(CarRepository::class);
    $cars = $carRepository->getEntities();

    $messages = $this->get('flash')->getMessages();

    $params = [
      'cars' => $cars,
      'flash' => $messages
    ];

    return $this->get('renderer')->render($response, 'cars/index.phtml', $params);
})->setName('cars.index');

$app->get('/cars/{id}', function ($request, $response, $args) {
    $carRepository = $this->get(CarRepository::class);
    $id = $args['id'];
    $car = $carRepository->find($id);

    if (is_null($car)) {
        return $response->write('Page not found')->withStatus(404);
    }

    $messages = $this->get('flash')->getMessages();

    $params = [
        'car' => $car,
        'flash' => $messages
    ];

    return $this->get('renderer')->render($response, 'cars/show.phtml', $params);
})->setName('cars.show');

$app->get('/cars/new', function ($request, $response) {
    $params = [
        'car' => new Car(),
        'errors' => []
    ];

    return $this->get('renderer')->render($response, 'cars/new.phtml', $params);
})->setName('cars.create');

$app->post('/cars', function ($request, $response) use ($router) {
    $carRepository = $this->get(CarRepository::class);
    $carData = $request->getParsedBodyParam('car');

    $validator = new CarValidator();
    $errors = $validator->validate($carData);

    if (count($errors) === 0) {
        $car = Car::fromArray([$carData['make'], $carData['model']]);
        $carRepository->save($car);
        $this->get('flash')->addMessage('success', 'Car was added successfully');
        return $response->withRedirect($router->urlFor('cars.index'));
    }

    $params = [
        'car' => $carData,
        'errors' => $errors
    ];

    return $this->get('renderer')->render($response->withStatus(422), 'cars/new.phtml', $params);
})->setName('cars.store');

Контейнер внедрения зависимостей достаточно умный и может сам создать объект репозитория. Он использует информацию из конструктора класса, чтобы автоматически внедрить нужные зависимости, когда они требуются. Нам остается только запросить объект репозитория из контейнера:

<?php

$carRepository = $this->get(CarRepository::class);

Когда мы запрашиваем объект из контейнера, контейнер видит, что CarRepository нуждается в PDO и автоматически создает экземпляр, передав ему соединение


Самостоятельная работа

  1. Проделайте все шаги из урока на своем компьютере
  2. Добавьте в приложение возможность удаления автомобиля из базы данных

Дополнительные материалы

  1. База данных SQLite
  2. Пример готового приложения

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Программирование на PHP, Разработка веб-приложений и сервисов используя Laravel, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

Задавайте вопросы, если хотите обсудить теорию или упражнения. Команда поддержки Хекслета и опытные участники сообщества помогут найти ответы и решить задачу