Skip to content
IvanKalashnikov edited this page Aug 30, 2022 · 27 revisions

Оглавление

С чего начать?

Модель

Модель - это класс, который наследуется от \Leadvertex\Plugin\Components\Db\Model и позволяет сохранять объект в базу.

Существует три возможных сценария использования:

  • для любого класса (ниже пример User)
  • для классов плагина (ниже пример Shipment)
  • для классов плагина, которые имеют один экземпляр (ниже пример Settings)

Давайте напишем модели для каждого из сценариев.

Реализация модели User

В качестве примера, напишем класс User, у которого, есть два поля: $name и $age:

class User extends \Leadvertex\Plugin\Components\Db\Model
{
    public string $name;
    public int $age;

    public function getId()
    {
        return $this->id;
    }

    public function setId($uuid)
    {
        $this->id = $uuid;
    }

    public static function schema(): array
    {
        return [
            'name' => ['VARCHAR(255)'],
            'age' => ['INT'],
        ];
    }
}

Класс должен наследоваться от Model. Как вы могли заметить, мы написали метод public static function schema(): array. Данный метод, возвращает схему, по которой будет создана таблица в базе. В ней должны содержаться все поля класса, которые необходимо записывать в базу. Важное уточнение, поле, которое необходимо сохранять в базу не может быть private. Подробнее о схеме в соответствующем разделе документации. Так же, мы инициализируем поле $id, которое объявлено в классе Model. Обратите внимание, что поле $id, намерено не было добавлено в схему, так как оно добавится автоматически. Рекомендуется, в $id передавать сгенерированный uuid, например, при помощи \Leadvertex\Plugin\Components\Db\Helpers\UuidHelper, как в следующем примере записи. Но прежде чем записать модель в базу, должна быть создана таблица, при помощи консольной команды php console.php db:create-tables подробнее в соответствующем разделе документации.

Пример записи модели User в базу и чтения из нее:

$uuid = \Leadvertex\Plugin\Components\Db\Helpers\UuidHelper::getUuid();

$model = new User();
$model->setId($uuid);
$model->name = 'Sasha';
$model->age = 25;
$model->save();

$id = $model->getId();
$findModel = User::findById($id);
echo $findModel->name; // возвращает 'Sasha'
echo $findModel->age; // возвращает '25'

После чего, можно создать объект класса User,$model = new User(), и установив в поля, необходимые значения $model->name = 'Sasha' и $model->age = 25, сохранить, используя $model->save(). В базе появится запись:

id name age
0f8169f8-ffb6-4801-8791-8008476acbeb Sasha 25

Для того, что-бы найти модель, можно воспользоваться методами для поиска, объявленными в классе Model. Давайте попробуем найти сохраненную модель:

$findModelById = User::findById(1);
echo $findModelById->name; // возвращает 'Sasha'
echo $findModelById->age; // возвращает '25'
echo $findModelById->getId(); // возвращает '1'

$findModelByIds = User::findByIds([1, 2, 3, 4]); // массив индексируется в соответствии с id
echo $findModelById[1]->name; // возвращает 'Sasha'
echo $findModelById[1]->age; // возвращает '25'
echo $findModelById[1]->getId(); // возвращает '1'

$findModelByCondition = User::findByCondition(['name' => 'Sasha']); // массив индексируется в соответствии с id
echo $findModelByCondition[1]->name; // возвращает 'Sasha'
echo $findModelByCondition[1]->age; // возвращает '25'
echo $findModelByCondition[1]->getId(); // возвращает '1'

Подробнее об этих методах и их особенностях в соответствующем разделе документации.

Реализация модели Shipment

В качестве примера, напишем класс Shipment, у которого, есть два поля: $address и $postcode:

class Shipment extends Model implements \Leadvertex\Plugin\Components\Db\PluginModelInterface
{

    public string $address;
    public string $postcode;

    public function __construct($id, string $address, string $postcode)
    {
        $this->id = $id;
        $this->postcode = $postcode;
        $this->address = $address;
    }

    public function getAddress(): string
    {
        return $this->address;
    }

    public function getPostcode(): string
    {
        return $this->postcode;
    }

    public static function schema(): array
    {
        return [
            'address' => ['VARCHAR(255)'],
            'postcode' => ['VARCHAR(255)'],
        ];
    }
}

Класс должен наследоваться от Model и реализовывать интерфейс \Leadvertex\Plugin\Components\Db\PluginModelInterface. В Model, для PluginModelInterface создаются дополнительные поля, а именно:

  • companyId
  • pluginAlias
  • pluginId

Аналогично прошлому примеру, поля должны быть public или protected. Обратите внимание, что в schema(), мы не объявляем id, companyId, pluginAlias и pluginId. Они будут добавлены автоматически. Пример записи модели Shipment в базу и чтения из нее:

$companyId = 1;
$pluginAlias = 'user';
$pluginId = 1;
$reference = new \Leadvertex\Plugin\Components\Db\Components\PluginReference($companyId, $pluginAlias, $pluginId);
Connector::setReference($reference);

$shipmentModel = new Shipment(1, 'Moscow', '123456');
$shipmentModel->save();

$findModel = Shipment::findById(1);
echo $findModel->getAddress(); // возвращает 'Moscow'
echo $findModel->getPostcode(); // возвращает '123456'

Аналогично примеру с User, инициализируем бд и создаем таблицу в ней, для нашей модели. Но теперь еще и устанавливаем ссылку на плагин. Делаем это при помощи статичного метода setReference в классе Connector. Передаем в него объект \Leadvertex\Plugin\Components\Db\Components\PluginReference, который в себе содержит три поля:

  • companyId - Идентификатор компании, которая использует плагин
  • pluginAlias - Псевдоним плагина(от чьего имени исполняется)
  • pluginId - Идентификатор плагина

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

companyId pluginAlias pluginId id address postcode
1 user 1 1 Moscow 123456

В таблице видно, что в базе модель, помимо, добавленных нами address и postcode, содержит дополнительные столбцы, как уже упоминалось, они добавляются автоматически.

Давайте изменим ссылку на плагин:

Connector::setReference(new PluginReference(2, $pluginAlias, $pluginId));

После чего сохраним еще одну модель, с теми же значениями:

$newShipmentModel = new Shipment(1, 'Moscow', '123456');
$newShipmentModel->save();

После этого в базе появится запись, в которой отличается только companyId, который мы изменили в setReference:

companyId pluginAlias pluginId id address postcode
1 user 1 1 Moscow 123456
2 user 1 1 Moscow 123456

Модели сохранялись с одинаковыми полями и $id, однако, в базе, companyId у них разные.

Добавим еще одну модель:

$newShipmentModel = new Shipment(2, 'New York', '123456');
$newShipmentModel->save();

В базе:

companyId pluginAlias pluginId id address postcode
1 user 1 1 Moscow 123456
2 user 1 1 Moscow 123456
2 user 1 2 New York 123456

Давайте попробуем найти модели:

$findModelById = Shipment::findById(1);
echo $findModelById->address; // возвращает 'Moscow'
echo $findModelById->postcode; // возвращает '123456'
echo $findModelById->getId(); // возвращает '1'

$findModelByIds = Shipment::findByIds([1, 2, 3, 4]); // массив индексируется в соответствии с id
echo $findModelById[1]->address; // возвращает 'Moscow'
echo $findModelById[1]->postcode; // возвращает '123456'
echo $findModelById[1]->getId(); // возвращает '1'
echo $findModelById[2]->address; // возвращает 'New York'
echo $findModelById[2]->postcode; // возвращает '123456'
echo $findModelById[2]->getId(); // возвращает '1'

$findModelByCondition = Post::findByCondition(['address' => 'New York']); // массив индексируется в соответствии с id
echo $findModelByCondition[2]->name; // возвращает 'New York'
echo $findModelByCondition[2]->age; // возвращает '123456'
echo $findModelByCondition[2]->getId(); // возвращает '2'

Сменим ссылку на плагин:

Connector::setReference(new PluginReference(1, $pluginAlias, $pluginId));

По пробуем теперь найти модели:

$findModelById = Shipment::findById(1);
echo $findModelById->address; // возвращает 'Moscow'
echo $findModelById->postcode; // возвращает '123456'
echo $findModelById->getId(); // возвращает '1'

$findModelByIds = Shipment::findByIds([1, 2, 3, 4]); // массив индексируется в соответствии с id
echo $findModelById[1]->address; // возвращает 'Moscow'
echo $findModelById[1]->postcode; // возвращает '123456'
echo $findModelById[1]->getId(); // возвращает '1'

$findModelByCondition = Post::findByCondition(['address' => 'New York']); // массив индексируется в соответствии с id
print_r($findModelByCondition); // возвращает 'Array()' 

Как видите, находятся только те модели, для которой ссылка на плагин соответствует установленной в Connector::setReference(). Происходит так из-за того, что для PluginModelInterface, в качестве уникального идентификатора служат поля companyId + pluginAlias + pluginId + id и при выполнении записи или чтения модели, они подставляются автоматически. Необходимо это, для того, что-бы автоматически предотвращать случайный доступ к чужим данным(другой компании или пользователя).

Что произойдет, если попробовать удалить найденную модель?

$findModelById = Shipment::findById(1);
$findModelById->delete();

В базе:

companyId pluginAlias pluginId id address postcode
2 user 1 1 Moscow 123456
2 user 1 2 New York 123456

То есть удалилась модель с id = 1, но только для компании с companyId = 1, так как именно для нее установленна ссылка на плагин. Данные другой компании, не изменились.

Реализация модели Settings

В качестве примера, напишем класс Settings, у которого, есть два поля: $deliveryType и $packType:

class Settings extends Model implements \Leadvertex\Plugin\Components\Db\SinglePluginModelInterface
{

    public string $deliveryType;

    public string $packType;

    public static function schema(): array
    {
        return [
            'deliveryType' => ['VARCHAR(255)'],
            'packType' => ['VARCHAR(255)'],
        ];
    }
}

Класс должен наследоваться от Model и реализовывать интерфейс \Leadvertex\Plugin\Components\Db\SinglePluginModelInterface]. Аналогично прошлым примерам, пишем метод schema(). В данном примере не инициализируем поле $id специально, так как объект данного класса может быть только один в базе.

Давайте попробуем записать модель Settings в базу, после чего, найдем ее и изменим:

$companyId = 1;
$pluginAlias = 'user';
$pluginId = 1;
$reference = new PluginReference($companyId, $pluginAlias, $pluginId);
Connector::setReference($reference);

$settingModel = new Settings();
$settingModel->deliveryType = 'Посылка до 10 кг';
$settingModel->packType = 'Коробка "S"';
$settingModel->save();

$findModel = Settings::find();
echo $findModel->deliveryType; // возвращает 'Посылка до 10 кг'
echo $findModel->packType; // возвращает 'Коробка "S"'

$findModel = Settings::find();
$findModel->deliveryType = 'Посылка до 5 кг';
$findModel->save();

echo Settings::find()->deliveryType; // возвращает 'Посылка до 5 кг'

Как видно в примере, поиск модели осуществляется с помощью метода find(), он не принимает ни каких параметров, так как в базе может существовать только один объект данного класса, для каждого набора companyId + pluginAlias + pluginId + id. То есть, если в Connector::setReference, передать PluginReference, с другим набором companyId, pluginAlias, pluginId, после чего, сохранить модель, то в базе, будет создана еще одна запись, соответствующая текущим companyId + pluginAlias + pluginId + id.

Схема

Схема - это массив, содержащий в себе название поля(индекс) и его тип(значение). Пример схемы:

[
    'name' => ['VARCHAR(255)'],
    'age' => ['INT'],
    'weight' => ['FLOAT']
];

Тип поля может быть любой, поддерживаемый ваше базой данных.

Создание таблиц в базе данных

Для этого создадимconsole.php:

<?php
require_once 'vendor/autoload.php';

use Leadvertex\Plugin\Components\Db\Commands\CreateTablesCommand;
use Leadvertex\Plugin\Components\Db\Components\Connector;
use Medoo\Medoo;
use Symfony\Component\Console\Application;

Connector::config(new Medoo([
    'database_type' => 'sqlite',
    'database_file' => __DIR__ . '/testDB.db'
]));

$application = new Application();
$application->add(new CreateTablesCommand());
$application->run();

И выполним консольную команду php console.php db:create-tables, команда создаст все таблицы, для всех моделей с пространством имен Leadvertex\Plugin. В примере, мы сначала инициализируем базу, при помощи класса \Leadvertex\Plugin\Components\Db\Components\Connector. Для этого, используем статичный метод config, в который передаем объект класса Medoo документация Medoo. Вы можете не писать свой console.php, а воспользоваться написанным, для этого просто выполните команду для создания таблиц.

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

Методы для поиска моделей

Есть 4 метода для поиска:

  • public static function findById(string $id): ?self метод принимает $id, и возвращает найденную модель.
  • public static function findByIds(array $ids): array метод принимает на вход массив $ids и возвращает массив найденных моделей.
  • public static function findByCondition(array $where): array метод принимает массив $where и возвращает массив найденных моделей.
  • public static function find(): ?Model - ничего не принимает и возвращает одну модель, может использоваться только для модели, которая имеет один экземпляр и реализуюет SinglePluginModelInterface.

Массив $where

Это массив условий для поиска. Подробнее о синтаксисе where в документации Medoo.

Приведем несколько примеров использования:

Пусть в таблице есть несколько моделей User

id name age
0f8169f8-ffb6-4801-8791-8008476acbeb Sasha 25
750cb54c-c35e-4e11-a9cd-37559cce7a0c Marina 25
7748576c-8d8f-4d70-8fa8-bc1a4cb78f9f Viktoria 18
f81c1ccb-72c4-4e97-91e4-8418320a43eb Daria 38
31fa349b-31f2-4947-80ea-c80a201118ec Dima 40
$models = User::findByCondition(['name' => 'Marina']);
print_r($models); /* возвращает
Array
(
    [750cb54c-c35e-4e11-a9cd-37559cce7a0c] => Leadvertex\Plugin\User Object
        (
            [name] => Marina
            [age] => 25
            [id:protected] => 750cb54c-c35e-4e11-a9cd-37559cce7a0c
            [_isNew:Leadvertex\Plugin\Components\Db\Model:private] => 
        )
)
*/

$models = User::findByCondition(['age' => '25']);
print_r($models); /* возвращает
Array
(
    [0f8169f8-ffb6-4801-8791-8008476acbeb] => Leadvertex\Plugin\User Object
        (
            [name] => Sasha
            [age] => 25
            [id:protected] => 0f8169f8-ffb6-4801-8791-8008476acbeb
            [_isNew:Leadvertex\Plugin\Components\Db\Model:private] => 
        )

    [750cb54c-c35e-4e11-a9cd-37559cce7a0c] => Leadvertex\Plugin\User Object
        (
            [name] => Marina
            [age] => 25
            [id:protected] => 750cb54c-c35e-4e11-a9cd-37559cce7a0c
            [_isNew:Leadvertex\Plugin\Components\Db\Model:private] => 
        )
)
*/

$models = User::findByCondition(['age[>]' => '30']);
print_r($models); /* возвращает
 Array
(
    [f81c1ccb-72c4-4e97-91e4-8418320a43eb] => Leadvertex\Plugin\User Object
        (
            [name] => Daria
            [age] => 38
            [id:protected] => f81c1ccb-72c4-4e97-91e4-8418320a43eb
            [_isNew:Leadvertex\Plugin\Components\Db\Model:private] => 
        )

    [31fa349b-31f2-4947-80ea-c80a201118ec] => Leadvertex\Plugin\User Object
        (
            [name] => Dima
            [age] => 40
            [id:protected] => 31fa349b-31f2-4947-80ea-c80a201118ec
            [_isNew:Leadvertex\Plugin\Components\Db\Model:private] => 
        )
)
*/

Особенности поиска классов плагина

У классов плагина, которые реализуют PluginModelInterface или SinglePluginModelInterface, как уже говорилось, автоматически добавляются поля companyId, pluginAlias и pluginId. При выполнении поиска, данные поля подставляются в каждый запрос, за счет этого, осуществляется безопасное взаимодействие с данными и исключается возможность получить доступ к чужим данным.

Особенности применения метода find()

Метод find() позволяет найти модель класса, объект которого может существовать в единственном экземпляре(в прим. Settings). Потому, что объект может быть только один, не требуется передавать $id или другую информацию о нем, так как поиск осуществляется по полям companyId, pluginAlias и pluginId. По этой причине, для подобных классов, стоит использовать только метод find(), как наиболее удобный. Учитывая, что остальные классы, которые могут иметь множество экземпляров(в прим. User и Shipment), не могут быть найдены только по полям companyId, pluginAlias и pluginId, использование метода find() с такими моделями невозможно.

Реализация сложных классов

Не редка ситуация, когда тип одного из полей вашего класса является сложным. Например, объект другого класса. Для записи и чтения такого поля, необходимо выполнить определенные преобразования в тип, поддерживаемый бд. Приведем примеры реализации подобного класса.

Допустим, у нас есть класс PostOffice:

class PostOffice
{

    private string $address;
    private string $postcode;

    public function __construct(string $address, string $postcode)
    {
        $this->postcode = $postcode;
        $this->address = $address;
    }
    
    public function getPostcode(): string
    {
        return $this->postcode;
    }

    public function getAddress(): string
    {
        return $this->address;
    }
}

Мы хотим сохранять его в формате json:

class Shipment extends Model implements PluginModelInterface
{

    public PostOffice $postOffice;

    public function setPostOffice(PostOffice $postOffice): void
    {
        $this->postOffice = $postOffice;
    }

    public function setId($id)
    {
        $this->id = $id;
    }

    public function getPostcode(): string
    {
        return $this->postOffice->getPostcode();
    }

    public function getAddress(): string
    {
        return $this->postOffice->getAddress();
    }

    public static function schema(): array
    {
        return [
            'postOffice' => ['VARCHAR(255)'],
        ];
    }

    protected static function beforeWrite(array $data): array
    {
        $postOffice = $data['postOffice'];
        $data['postOffice'] = json_encode(self::postOfficeToArray($postOffice));
        return $data;
    }

    protected static function afterRead(array $data): array
    {
        $postArray = json_decode($data['postOffice'], true);
        $data['postOffice'] = new PostOffice($postArray['address'], $postArray['postcode']);
        return $data;
    }

    private static function postOfficeToArray(PostOffice $postOffice): array
    {
        return ['address' => $postOffice->getAddress(), 'postcode' => $postOffice->getPostcode()];
    }
}

Как видно в примере, мы переопределили два метода beforeWrite и afterRead. Метод beforeWrite выполняется перед записью, а метод afterRead сразу после чтения. Оба метода принимают на вход массив $data, в котором содержатся все поля класса и возвращают его же.

Запишем модель в базу:

$companyId = 1;
$pluginAlias = 'user';
$pluginId = 1;
Connector::setReference(new PluginReference($companyId, $pluginAlias, $pluginId));

$postOffice = new PostOffice('City', '123456');
$shipmentModel = new Shipment();
$id = Uuid::uuid4();
$shipmentModel->setId($id);
$shipmentModel->setPostOffice($postOffice);
$shipmentModel->save();

В базе:

companyId pluginAlias pluginId id postOffice
1 user 1 1df95f06-c881-43fa-8cf6-6cd1c9e01f70 {"address":"City","postcode":"123456"}

И найдем ее:

$findModel = Post::findById($id);
echo $findModel->getAddress(); // возвращает 'City'
echo $findModel->getPostcode(); // возвращает '123456'

Использовать метод serialize не рекомендуется, т.к. может измениться версия php и может возникнуть ситуация, что данные в базе не получится прочитать. Лучше использовать формат json.

Класс Model

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

  • public function getId(): string
  • public function save(): void
  • public function delete(): void
  • public function isNewModel(): bool
  • protected function beforeSave(bool $isNew): void
  • protected function afterFind(): void
  • public static function findById(string $id): ?self
  • public static function findByIds(array $ids): array
  • public static function findByCondition(array $where): array
  • public static function find(): ?Model
  • public static function addOnSaveHandler(callable $handler, string $name = null): void
  • public static function removeOnSaveHandler(string $name): void
  • public static function tableName(): string
  • abstract public static function schema(): array;
  • public static function freeUpMemory(): void
  • protected static function afterRead(array $data): array
  • protected static function beforeWrite(array $data): array
  • protected static function db(): Medoo

Методы addOnSaveHandler и removeOnSaveHandler

Данные методы позволяют добавить и удалить обработчик(callable $handler). Вызов обработчика будет происходить во время сохранения модели в базу, то есть во время вызова метода save. Это может быть полезно, когда необходимо выполнить какие-то действия, непосредственно в момент сохранения модели.

Отличие методов beforeWrite, beforeSave и afterRead, afterFind

Методы beforeSave и afterFind ничего не возвращают, в отличие от рассмотренных выше beforeWrite и afterRead. Данные методы можно переопределить для выполнения любого, необходимого кода. При выполнении метода save или одного из find, сначала вызывается beforeWrite или afterRead, после чего beforeSave или afterFind, соответственно.

Метод afterTableCreate

Данный метод нужен для вызова любых sql-запросов после создания таблицы при вызове db:create-tables. Например, нам необходимо задать индексы для нашей таблицы (для sqlite нельзя сразу создать индексы при CREATE TABLE запросе) или же мы хотим заполнить таблицу какими-то данными.

Метод tableName

Данный метод является статическим и возвращает имя таблицы, в которой хранятся объекты класса.

Метод freeUpMemory

Данный метод является статическим и используется для очистки памяти.

Все найденные модели, хранятся в единственном экземпляре в оперативной памяти. То есть, если, например в базе:

id name age
0f8169f8-ffb6-4801-8791-8008476acbeb Sasha 25
31fa349b-31f2-4947-80ea-c80a201118ec Dima 40

И мы ищем модели:

$model_1 = User::findByCondition(['name' => 'Sasha']);
$model_2 = User::findByCondition(['age' => '25']);

Найденные модели будут идентичны:

$model_1 === $model_2;

В ситуации, когда вам нужно найти большое количество моделей(> 1000), может не хватить оперативной памяти. В таком случае, рекомендуется осуществлять поиск итеративно, выполняя freeUpMemory() после каждой итерации.

Класс Connector

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

  • public static function config(Medoo $medoo): void
  • public static function db(): Medoo
  • public static function hasReference(): bool
  • public static function getReference(): PluginReference
  • public static function setReference(PluginReference $reference)

Метод config

Данный метод принимает объект класса Medoo Документация Medoo и используется для инициализации базы, как в примерах выше.

Метод setReference

Данный метод принимает объект класса PluginReference и используется для создания ссылки на плагин, как в примерах выше.

Класс PluginReference

Это класс, который позволяет установить ссылку на плагин. Метод принимает три поля в конструктор:

  • companyId - Идентификатор компании, которая использует плагин.
  • pluginAlias - Псевдоним плагина(от чьего имени исполняется)
  • pluginId - Идентификатор плагина Данный класс используется в Connector.