Flight Active Record

Активная запись – это отображение сущности базы данных на объект PHP. Проще говоря, если у вас есть таблица пользователей в вашей базе данных, вы можете "перевести" строку в этой таблице на класс User и объект $user в вашем коде. См. основной пример.

Нажмите здесь для просмотра репозитория на GitHub.

Основной пример

Предположим, у вас есть следующая таблица:

CREATE TABLE users (
    id INTEGER PRIMARY KEY, 
    name TEXT, 
    password TEXT 
);

Теперь вы можете настроить новый класс для представления этой таблицы:

/**
 * Класс ActiveRecord обычно единственное число
 * 
 * Настоятельно рекомендуется добавлять свойства таблицы в виде комментариев здесь
 * 
 * @property int    $id
 * @property string $name
 * @property string $password
 */ 
class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        // вы можете установить это таким образом
        parent::__construct($database_connection, 'users');
        // или таким образом
        parent::__construct($database_connection, null, [ 'table' => 'users']);
    }
}

Теперь смотрите, как происходит магия!

// для sqlite
$database_connection = new PDO('sqlite:test.db'); // это просто для примера, вы, вероятно, используете реальное подключение к базе данных

// для mysql
$database_connection = new PDO('mysql:host=localhost;dbname=test_db&charset=utf8bm4', 'username', 'password');

// или mysqli
$database_connection = new mysqli('localhost', 'username', 'password', 'test_db');
// или mysqli с созданием без объекта
$database_connection = mysqli_connect('localhost', 'username', 'password', 'test_db');

$user = new User($database_connection);
$user->name = 'Bobby Tables';
$user->password = password_hash('некоторый классный пароль');
$user->insert();
// или $user->save();

echo $user->id; // 1

$user->name = 'Joseph Mamma';
$user->password = password_hash('некоторый классный пароль снова!!!');
$user->insert();
// нельзя использовать $user->save() здесь, иначе он подумает, что это обновление!

echo $user->id; // 2

И добавление нового пользователя было так же просто! Теперь, когда в базе данных есть строка пользователя, как вы можете ее извлечь?

$user->find(1); // найти id = 1 в базе данных и вернуть его.
echo $user->name; // 'Bobby Tables'

А что если вы хотите найти всех пользователей?

$users = $user->findAll();

Что насчет определенного условия?

$users = $user->like('name', '%mamma%')->findAll();

Видите, как это весело? Давайте установим это и начнем!

Установка

Просто установите с помощью Composer

composer require flightphp/active-record 

Использование

Это можно использовать как отдельную библиотеку или с PHP-фреймворком Flight. Полностью вам решать.

Отдельно

Просто убедитесь, что вы передаете подключение PDO в конструктор.

$pdo_connection = new PDO('sqlite:test.db'); // это просто для примера, вы, вероятно, используете реальное подключение к базе данных

$User = new User($pdo_connection);

Не хотите всегда устанавливать ваше подключение к базе данных в конструкторе? См. Управление подключением к базе данных для других идей!

Зарегистрировать как метод в Flight

Если вы используете PHP-фреймворк Flight, вы можете зарегистрировать класс ActiveRecord как сервис, но вам честно не обязательно это делать.

Flight::register('user', 'User', [ $pdo_connection ]);

// тогда вы можете использовать это так в контроллере, функции и т.д.

Flight::user()->find(1);

Методы runway

runway – это CLI инструмент для Flight, который имеет специальную команду для этой библиотеки.

# Использование
php runway make:record database_table_name [class_name]

# Пример
php runway make:record users

Это создаст новый класс в директории app/records/ как UserRecord.php со следующим содержимым:

<?php

declare(strict_types=1);

namespace app\records;

/**
 * Класс ActiveRecord для таблицы пользователей.
 * @link https://docs.flightphp.com/awesome-plugins/active-record
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password_hash
 * @property string $created_dt
 */
class UserRecord extends \flight\ActiveRecord
{
    /**
     * @var array $relations Установите отношения для модели
     *   https://docs.flightphp.com/awesome-plugins/active-record#relationships
     */
    protected array $relations = [
        // 'relation_name' => [ self::HAS_MANY, 'RelatedClass', 'foreign_key' ],
    ];

    /**
     * Конструктор
     * @param mixed $databaseConnection Подключение к базе данных
     */
    public function __construct($databaseConnection)
    {
        parent::__construct($databaseConnection, 'users');
    }
}

Функции CRUD

find($id = null) : boolean|ActiveRecord

Находит одну запись и присваивает ее текущему объекту. Если вы передаете $id какого-либо рода, это будет выполнять поиск по первичному ключу с этим значением. Если ничего не передается, это просто найдет первую запись в таблице.

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

// найти запись с некоторыми условиями заранее
$user->notNull('password')->orderBy('id DESC')->find();

// найти запись по конкретному id
$id = 123;
$user->find($id);

findAll(): array<int,ActiveRecord>

Находит все записи в таблице, которую вы указываете.

$user->findAll();

isHydrated(): boolean (v0.4.0)

Возвращает true, если текущая запись была гидратирована (извлечена из базы данных).

$user->find(1);
// если запись найдена с данными...
$user->isHydrated(); // true

insert(): boolean|ActiveRecord

Вставляет текущую запись в базу данных.

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->insert();
Первичные ключи на текстовой основе

Если у вас есть первичный ключ на текстовой основе (например, UUID), вы можете установить значение первичного ключа перед вставкой одним из двух способов.

$user = new User($pdo_connection, [ 'primaryKey' => 'uuid' ]);
$user->uuid = 'some-uuid';
$user->name = 'demo';
$user->password = md5('demo');
$user->insert(); // или $user->save();

или вы можете позволить первичному ключу автоматически генерироваться для вас через события.

class User extends flight\ActiveRecord {
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users', [ 'primaryKey' => 'uuid' ]);
        // вы также можете установить первичный ключ таким образом вместо массива выше.
        $this->primaryKey = 'uuid';
    }

    protected function beforeInsert(self $self) {
        $self->uuid = uniqid(); // или как вам нужно сгенерировать ваши уникальные идентификаторы
    }
}

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

update(): boolean|ActiveRecord

Обновляет текущую запись в базе данных.

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@example.com';
$user->update();

save(): boolean|ActiveRecord

Вставляет или обновляет текущую запись в базе данных. Если у записи есть id, она будет обновлена, в противном случае будет вставлена.

$user = new User($pdo_connection);
$user->name = 'demo';
$user->password = md5('demo');
$user->save();

Примечание: Если у вас есть определенные отношения в классе, они также будут рекурсивно сохранены, если они были определены, созданы и имеют измененные данные для обновления. (v0.4.0 и выше)

delete(): boolean

Удаляет текущую запись из базы данных.

$user->gt('id', 0)->orderBy('id desc')->find();
$user->delete();

Вы также можете удалить несколько записей, исполняя поиск заранее.

$user->like('name', 'Bob%')->delete();

dirty(array $dirty = []): ActiveRecord

Грязные данные относятся к данным, которые были изменены в записи.

$user->greaterThan('id', 0)->orderBy('id desc')->find();

// на данный момент ничего не "грязное".

$user->email = 'test@example.com'; // теперь email считается "грязным", поскольку он был изменен.
$user->update();
// теперь нет грязных данных, так как они были обновлены и сохранены в базе данных

$user->password = password_hash('newpassword'); // теперь это грязное
$user->dirty(); // ничего не передав, вы очистите все грязные записи.
$user->update(); // ничего не обновится, потому что ничего не было захвачено как грязное.

$user->dirty([ 'name' => 'что-то', 'password' => password_hash('другой пароль') ]);
$user->update(); // обновлены как имя, так и пароль.

copyFrom(array $data): ActiveRecord (v0.4.0)

Это псевдоним для метода dirty(). Немного более понятно, что вы делаете.

$user->copyFrom([ 'name' => 'что-то', 'password' => password_hash('другой пароль') ]);
$user->update(); // обновлены как имя, так и пароль.

isDirty(): boolean (v0.4.0)

Возвращает true, если текущая запись была изменена.

$user->greaterThan('id', 0)->orderBy('id desc')->find();
$user->email = 'test@email.com';
$user->isDirty(); // true

reset(bool $include_query_data = true): ActiveRecord

Сбрасывает текущую запись в ее начальное состояние. Это действительно полезно использовать в поведениях типа цикла. Если вы передадите true, он также сбросит данные запроса, которые использовались для поиска текущего объекта (поведение по умолчанию).

$users = $user->greaterThan('id', 0)->orderBy('id desc')->find();
$user_company = new UserCompany($pdo_connection);

foreach($users as $user) {
    $user_company->reset(); // начинаем с чистого листа
    $user_company->user_id = $user->id;
    $user_company->company_id = $some_company_id;
    $user_company->insert();
}

getBuiltSql(): string (v0.4.1)

После выполнения метода find(), findAll(), insert(), update() или save() вы можете получить SQL, который был построен, и использовать его для отладки.

Методы SQL Запросов

select(string $field1 [, string $field2 ... ])

Вы можете выбрать только несколько столбцов в таблице, если хотите (это более производительно на действительно широких таблицах с большим количеством столбцов)

$user->select('id', 'name')->find();

from(string $table)

Вы вполне можете выбрать другую таблицу! Почему бы и нет?!

$user->select('id', 'name')->from('user')->find();

join(string $table_name, string $join_condition)

Вы даже можете присоединиться к другой таблице в базе данных.

$user->join('contacts', 'contacts.user_id = users.id')->find();

where(string $where_conditions)

Вы можете установить некоторые пользовательские аргументы where (вы не можете устанавливать параметры в этом условии where)

$user->where('id=1 AND name="demo"')->find();

Примечание по безопасности - Вам может возникнуть желание сделать что-то вроде $user->where("id = '{$id}' AND name = '{$name}'")->find();. Пожалуйста, НЕ ДЕЛАЙТЕ ЭТОГО!!! Это подвержено тому, что называется атаками SQL-инъекций. В Интернете есть много статей, пожалуйста, поищите "sql injection attacks php", и вы найдёте много статей на эту тему. Правильный способ обработки этого с помощью этой библиотеки вместо этого метода where(), вы бы сделали что-то более вроде $user->eq('id', $id)->eq('name', $name)->find(); Если вам абсолютно необходимо это делать, библиотека PDO имеет $pdo->quote($var), чтобы экранировать это для вас. Только после того, как вы используете quote(), вы можете использовать это в операторе where().

group(string $group_by_statement)/groupBy(string $group_by_statement)

Группируйте ваши результаты по определенному условию.

$user->select('COUNT(*) as count')->groupBy('name')->findAll();

order(string $order_by_statement)/orderBy(string $order_by_statement)

Сортируйте возвращаемый запрос определённым образом.

$user->orderBy('name DESC')->find();

limit(string $limit)/limit(int $offset, int $limit)

Ограничьте количество возвращаемых записей. Если задано второе целое число, оно будет сдвинуто, ограничение также как в SQL.

$user->orderby('name DESC')->limit(0, 10)->findAll();

Условия WHERE

equal(string $field, mixed $value) / eq(string $field, mixed $value)

Где field = $value

$user->eq('id', 1)->find();

notEqual(string $field, mixed $value) / ne(string $field, mixed $value)

Где field <> $value

$user->ne('id', 1)->find();

isNull(string $field)

Где field IS NULL

$user->isNull('id')->find();

isNotNull(string $field) / notNull(string $field)

Где field IS NOT NULL

$user->isNotNull('id')->find();

greaterThan(string $field, mixed $value) / gt(string $field, mixed $value)

Где field > $value

$user->gt('id', 1)->find();

lessThan(string $field, mixed $value) / lt(string $field, mixed $value)

Где field < $value

$user->lt('id', 1)->find();

greaterThanOrEqual(string $field, mixed $value) / ge(string $field, mixed $value) / gte(string $field, mixed $value)

Где field >= $value

$user->ge('id', 1)->find();

lessThanOrEqual(string $field, mixed $value) / le(string $field, mixed $value) / lte(string $field, mixed $value)

Где field <= $value

$user->le('id', 1)->find();

like(string $field, mixed $value) / notLike(string $field, mixed $value)

Где field LIKE $value или field NOT LIKE $value

$user->like('name', 'de')->find();

in(string $field, array $values) / notIn(string $field, array $values)

Где field IN($value) или field NOT IN($value)

$user->in('id', [1, 2])->find();

between(string $field, array $values)

Где field BETWEEN $value AND $value1

$user->between('id', [1, 2])->find();

Условия ИЛИ

Возможно, вы захотите обернуть ваши условия в оператор ИЛИ. Это делается либо с помощью методов startWrap() и endWrap(), либо путем заполнения 3-го параметра условия после поля и значения.

// Метод 1
$user->eq('id', 1)->startWrap()->eq('name', 'demo')->or()->eq('name', 'test')->endWrap('OR')->find();
// Это будет эквивалентно `id = 1 AND (name = 'demo' OR name = 'test')`

// Метод 2
$user->eq('id', 1)->eq('name', 'demo', 'OR')->find();
// Это будет эквивалентно `id = 1 OR name = 'demo'`

Отношения

Вы можете установить несколько видов отношений, используя эту библиотеку. Вы можете установить отношения один->много и один->один между таблицами. Это требует небольшой дополнительной настройки в классе заранее.

Установка массива $relations не сложна, но угадать правильный синтаксис может быть сложно.

protected array $relations = [
    // вы можете назвать ключ как угодно. Имя ActiveRecord, вероятно, хорошее. Например: user, contact, client
    'user' => [
        // обязательно
        // self::HAS_MANY, self::HAS_ONE, self::BELONGS_TO
        self::HAS_ONE, // это тип отношения

        // обязательно
        'Some_Class', // это "другой" ActiveRecord, на который это будет ссылаться

        // обязательно
        // в зависимости от типа отношения
        // self::HAS_ONE = внешний ключ, который ссылается на соединение
        // self::HAS_MANY = внешний ключ, который ссылается на соединение
        // self::BELONGS_TO = локальный ключ, который ссылается на соединение
        'local_or_foreign_key',
        // просто FYI, это также соединяется только с первичным ключом "другой" модели

        // опционально
        [ 'eq' => [ 'client_id', 5 ], 'select' => 'COUNT(*) as count', 'limit' 5 ], // дополнительные условия, которые вы хотите при соединении с отношением
        // $record->eq('client_id', 5)->select('COUNT(*) as count')->limit(5))

        // опционально
        'back_reference_name' // это если вы хотите ссылаться на это отношение обратно на себя, например: $user->contact->user;
    ];
]
class User extends ActiveRecord{
    protected array $relations = [
        'contacts' => [ self::HAS_MANY, Contact::class, 'user_id' ],
        'contact' => [ self::HAS_ONE, Contact::class, 'user_id' ],
    ];

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }
}

class Contact extends ActiveRecord{
    protected array $relations = [
        'user' => [ self::BELONGS_TO, User::class, 'user_id' ],
        'user_with_backref' => [ self::BELONGS_TO, User::class, 'user_id', [], 'contact' ],
    ];
    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'contacts');
    }
}

Теперь у нас есть установленные ссылки, так что мы можем использовать их очень легко!

$user = new User($pdo_connection);

// найдите самого последнего пользователя.
$user->notNull('id')->orderBy('id desc')->find();

// получите контакты, используя отношение:
foreach($user->contacts as $contact) {
    echo $contact->id;
}

// или мы можем пойти другим путем.
$contact = new Contact();

// найдите один контакт
$contact->find();

// получите пользователя, используя отношение:
echo $contact->user->name; // это имя пользователя

Классно, да?

Установка пользовательских данных

Иногда вам может потребоваться прикрепить что-то уникальное к вашей ActiveRecord, например, пользовательский расчет, который, возможно, будет легче просто прикрепить к объекту, который затем будет передан, скажем, шаблону.

setCustomData(string $field, mixed $value)

Вы прикрепляете пользовательские данные с помощью метода setCustomData().

$user->setCustomData('page_view_count', $page_view_count);

А затем вы просто ссылаетесь на это, как на обычное свойство объекта.

echo $user->page_view_count;

События

Одним из супер классных преимуществ этой библиотеки являются события. События срабатывают в определенные моменты на основе определенных методов, которые вы вызываете. Они очень полезны для автоматической настройки данных для вас.

onConstruct(ActiveRecord $ActiveRecord, array &config)

Это действительно полезно, если вам нужно установить подключение по умолчанию или что-то подобное.

// index.php или bootstrap.php
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

//
//
//

// User.php
class User extends flight\ActiveRecord {

    protected function onConstruct(self $self, array &$config) { // не забывайте о референции &
        // вы можете сделать это, чтобы автоматически установить подключение
        $config['connection'] = Flight::db();
        // или это
        $self->transformAndPersistConnection(Flight::db());

        // Вы также можете установить имя таблицы таким образом.
        $config['table'] = 'users';
    } 
}

beforeFind(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFind(self $self) {
        // всегда выполняйте id >= 0, если это ваше желание
        $self->gte('id', 0); 
    } 
}

afterFind(ActiveRecord $ActiveRecord)

Эта один, вероятно, более полезна, если вам всегда нужно выполнять какую-либо логику каждый раз, когда эта запись извлекается. Нужно ли вам расшифровать что-то? Вам нужно запустить пользовательский запрос на подсчет каждый раз (невыгодно, но что ж)?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFind(self $self) {
        // расшифровка чего-то
        $self->secret = yourDecryptFunction($self->secret, $some_key);

        // возможно, сохранение чего-то пользовательского, как запрос???
        $self->setCustomData('view_count', $self->select('COUNT(*) count')->from('user_views')->eq('user_id', $self->id)['count']); 
    } 
}

beforeFindAll(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeFindAll(self $self) {
        // всегда выполняйте id >= 0, если это ваше желание
        $self->gte('id', 0); 
    } 
}

afterFindAll(array<int,ActiveRecord> $results)

Похоже на afterFind(), но вы можете делать это со всеми записями сразу!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterFindAll(array $results) {

        foreach($results as $self) {
            // сделайте что-то классное, как и в afterFind()
        }
    } 
}

beforeInsert(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeInsert(self $self) {
        // задайте некоторые надежные значения по умолчанию
        if(!$self->created_date) {
            $self->created_date = gmdate('Y-m-d');
        }

        if(!$self->password) {
            $self->password = password_hash((string) microtime(true));
        }
    } 
}

afterInsert(ActiveRecord $ActiveRecord)

Возможно, у вас есть случай использования для изменения данных послеINSERT?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterInsert(self $self) {
        // делайте что хотите
        Flight::cache()->set('most_recent_insert_id', $self->id);
        // или что-то еще...
    } 
}

beforeUpdate(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeUpdate(self $self) {
        // задайте некоторые надежные значения по умолчанию
        if(!$self->updated_date) {
            $self->updated_date = gmdate('Y-m-d');
        }
    } 
}

afterUpdate(ActiveRecord $ActiveRecord)

Возможно, у вас есть случай использования для изменения данных после его обновления?

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function afterUpdate(self $self) {
        // делайте что хотите
        Flight::cache()->set('most_recently_updated_user_id', $self->id);
        // или что-то еще...
    } 
}

beforeSave(ActiveRecord $ActiveRecord)/afterSave(ActiveRecord $ActiveRecord)

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

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeSave(self $self) {
        $self->last_updated = gmdate('Y-m-d H:i:s');
    } 
}

beforeDelete(ActiveRecord $ActiveRecord)/afterDelete(ActiveRecord $ActiveRecord)

Не уверен, что вы хотите сделать здесь, но никаких предвзятостей! Дерзайте!

class User extends flight\ActiveRecord {

    public function __construct($database_connection)
    {
        parent::__construct($database_connection, 'users');
    }

    protected function beforeDelete(self $self) {
        echo 'Он был смелым солдатом... :cry-face:';
    } 
}

Управление подключением к базе данных

Когда вы используете эту библиотеку, вы можете установить подключение к базе данных несколькими способами. Вы можете установить подключение в конструкторе, вы можете установить его через переменную конфигурации $config['connection'] или вы можете установить его через setDatabaseConnection() (v0.4.1).

$pdo_connection = new PDO('sqlite:test.db'); // например
$user = new User($pdo_connection);
// или
$user = new User(null, [ 'connection' => $pdo_connection ]);
// или
$user = new User();
$user->setDatabaseConnection($pdo_connection);

Если вы хотите избежать постоянного указания $database_connection каждый раз, когда вы вызываете активную запись, есть способы обойти это!

// index.php или bootstrap.php
// Установите это как зарегистрированный класс в Flight
Flight::register('db', 'PDO', [ 'sqlite:test.db' ]);

// User.php
class User extends flight\ActiveRecord {

    public function __construct(array $config = [])
    {
        $database_connection = $config['connection'] ?? Flight::db();
        parent::__construct($database_connection, 'users', $config);
    }
}

// И теперь, без аргументов не требуется!
$user = new User();

Примечание: Если вы планируете unit-тестирование, делать это может создать некоторые проблемы с unit-тестами, но в целом, потому что вы можете инъектировать ваше подключение с помощью setDatabaseConnection() или $config['connection'], это не слишком плохо.

Если вам нужно обновить подключение к базе данных, например, если вы запускаете долгое CLI-скрипт и вам нужно периодически обновлять подключение, вы можете переустановить соединение с помощью $your_record->setDatabaseConnection($pdo_connection).

Участие

Пожалуйста, сделайте это. :D

Настройка

Когда вы участвуете, убедитесь, что вы выполняете команду composer test-coverage, чтобы поддерживать 100% покрытие тестами (это не истинное покрытие юнит-тестами, больше похоже на интеграционное тестирование).

Также убедитесь, что вы выполняете composer beautify и composer phpcs, чтобы исправить любые ошибки линтинга.

Лицензия

MIT