- Устанавливаем зависимости
- Настраиваем подключение
- Строим начальную структуру базы данных
- Создаем репозиторий CarRepository
- Рассматриваем примеры операций
Репозитории, которыми мы пользовались в уроках этого курса, хранят свои данные в сессии. Это было удобно для того, чтобы не отвлекаться на взаимодействие с базой и сфокусироваться на особенностях работы веба.
Теперь мы привыкли к фреймворку и понимаем принципы создания приложений на нем, поэтому можно начинать работу с реальной базой данных.
Чтобы начать хранить и извлекать данные из базы, нам нужно выполнить несколько действий:
- Установить зависимости, необходимые для работы с базой данных
- Настроить подключение к базе данных и дать к нему доступ из приложения
- Создать начальную структуру базы данных с нужными таблицами
- Переписать методы репозиториев так, чтобы они работали с данными через базу
В этом уроке мы проделаем все эти шаги на примере создания части 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-репозиторий.
Строим начальную структуру базы данных
В нашем случае база данных создается при старте приложения, поэтому ее инициализацию мы будем делать там же, во время старта. Для этого создадим файл с нужной схемой данных и затем добавим ее в базу данных:
Добавляем файл init.sql:
CREATE TABLE IF NOT EXISTS cars ( id INTEGER PRIMARY KEY AUTOINCREMENT, make VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL );
Загружаем схему в базу:
<?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
и автоматически создает экземпляр, передав ему соединение
Самостоятельная работа
- Проделайте все шаги из урока на своем компьютере
- Добавьте в приложение возможность удаления автомобиля из базы данных
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.