Паттерн проектирования Repository очень полезен при разработке веб-приложений. В его основе заложен класс Entity, который представляет собой единый объект любых данных в вашей модели. Это может быть один пользователь, комментарий, статья или что-нибудь еще в вашем приложении.

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

Получение объектов из модели

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

Сделать это так же просто, как прохождение полностью квалифицированного названия класса как единственного параметра к методу getResult():

$rows = $db->table('users')->limit(20)->get();
$user = $rows->getResult('App\Entities\User');

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

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

UserModel может выглядеть следующим образом:

<?php namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
    protected $table = 'users';
    protected $returnType = 'App\Entities\User';
    protected $useSoftDeletes = false;
    protected $allowedFields = [
        'username', 'email', 'password_hash'
    ];
    protected $useTimestamps = true;
}

Обратите внимание на свойство $returnType. Установив класс Entity, все наши результаты будут возвращены как экземпляры App\Entities\User по умолчанию. Мы всегда можем это изменить во время выполнения.

Кроме того, обратите внимание на свойство $allowedFields. Это особенно важно по нескольким причинам. Во-первых, модели класса заставляет вас заполнить его, если вы хотите передать ему массив пар ключ /значение во время save (), insert (), или update () для защиты от атак массового назначения, или простой человеческой ошибки. Но это также пригодится, когда мы хотим  сохранить объект обратно в базу данных. Подробнее об этом чуть позже.

Entity Класс

Теперь давайте рассмотрим простейший вариант класса Entity, который мы можем сделать. Здесь создан новый файл в /application/Entities/User.php и код выглядит следующим образом:

<?php namespace App\Entities;

class User 
{
    public $id;
    public $username;
    public $email;
    public $password_hash;
    public $created_at;
    public $updated_at;

    public function setPassword(string $password)
    {
        $this->password_hash = password_hash($password, PASSWORD_DEFAULT);

        return $this;
    }

    public function createdOn($format = ‘Y-m-d’)
    {
        $date = new DateTime($this->created_at);
        return $date->format($format);
    }
}

Пример содержит свойства всех столбцов в базе данных, а не только те, которые перечислены в $allowedFields. Так, как они все публичные, то могут быть доступны извне класса, будь то в контроллере, отображении или в самой модели при сохранении.

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

Сохранение Entity

Модель класса CodeIgniter достаточно умна, чтобы распознавать классы Entity всякий раз, когда вы пытаетесь выполнить save(), update(), или insert(). Класс  преобразовывается в массив пар ключ/значение, которые могут быть использованы для создания или обновления записи в базе данных.

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

В нашем примере этот массив бы выглядел примерно так:

[
    'username' => 'blackjack',
    'email' => 'jack.black@example.com',
    'password_hash' => '...'
]

Стоит обратить внимание, что здесь нет полей id, created_at или update_at. А все потому, что поле id автоматически назначается базой данных, и мы не должны иметь возможность изменить его. Поля дат управляются самой моделью класса.

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

// Grab a user
$user = $userModel->find(15);
// Update their email
$user->email = 'blackjack@example.com';
// Save the user
$userModel->save($user);

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

Методы получения и установки

Первым шагом нужно сделать свойства класса protected вместо public. Для того, чтобы сделать модель видимой будем использовать магические методы __get() и __set() для обеспечения доступа.

public function __get(string $key)
{
    if (isset($this->$key))
    {
        return $this->$key;
    }
}

public function __set(string $key, $value = null)
{
    if (isset($this->$key))
        
    {

        $this->$key = $value;
      
    }
}

Это добавит дополнительный код между значением и вашим кодом, который хорош для инкапсулирования. В нем собираюется некоторая логика, которую необходимо выполнить несколько раз когда мы имеем значение на Entity. Например, вы можете автоматически хэшировать пароль всякий раз, когда он установлен. Или всегда сохранять экземпляры DateTime ваших дат.

Для этого, давайте добавим некоторые новые функциональные возможности сеттер , который позволяет нам вызывать любой метод, который имел set_, а затем имя свойства set_password.

public function __set(string $key, $value = null)
{
    // if a set* method exists for this key,
       
    // use that method to insert this value.
       
    $method = 'set_'.$key;
     
    if (method_exists($this, $method))
     
    {
          
        $this->$method($value);
        
    }
      
    // A simple insert on existing keys otherwise.
     
    elseif (isset($this->$key))
        
    {
          
        $this->$key = $value;
      
    }

}
protected function set_password(string $value)
{
    $this->password = password_hash($value, PASSWORD_BCRYPT);
}

protected function set_last_login(string $value)
{
    $this->last_login = new DateTime($value);
}

Всякий раз, когда вы устанавливаете $user->password = ‘abc’ или  $user->last_login = ’10-15-2016 3:42pm’ пользовательские методы будут автоматически именоваться, сохраняя свойства.

Делаем то же самое для _get:

public function __get(string $key)
 
{
          
    // if a set* method exists for this key,
       
    // use that method to insert this value.
       
    if (method_exists($this, $key))
        
    {
              
        return $this->$key();
      
    }

         
    if (isset($this->$key))
        
    {
          
        return $this->$key;
        
    }
  
}

В этом случае мы просто проверяем метод с именем свойства класса. Вы можете установить эти методы как public, а затем они будут работать так же, независимо от того, был ли он вызван как метод или свойство, user->last_login or $user->last_login();

public function last_login($format=‘Y-m-d H:i:s’)
{
    return $this->last_login->format($format);
}


Установив значение по умолчанию для $format, теперь вы можете получить значение в нужном вам формате.

Filler

В следующем примере метод fill позволит просто подсунуть массив пар ключ/значение в класс для автоматического заполнения свойств. Он позволит захватить данных из $_POST, создать новый Entity класс и засунуть его туду перед сохранением.

public function fill(array $data)
  
{
          
    foreach ($data as $key => $var)
        
    {
              
        $method = 'fill_'. $key;
           
        if (method_exists($this, $method))
         
        {
              
            $this->$method($var);
          
        }

         
        elseif (property_exists($this, $key))
          
        {
              
            $this->$key = $var;
            
        }
      
    }
  
}

Захватим данные POST и добавим их в новый объект пользователя, затем сохраним его в базу данных:

$data = $_POST;
$user = new App\Entities\User();
$user->fill($data);
$userModel->save($user);

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

protected function fill_password(string $value)_
{
    $this->set_password($value);
}

Класс Entity

Чтобы сделать это все простым к повторному использованию, мы должны создать новый Entity класс, чтобы наши Entites можно было расширить и получить эти особенности автоматически. 

<?php namespace Myth;

/**
 * Class Entity
 *
 * A base class for entities that provides some convenience methods
 * that make working with entities a little simpler.
 *
 * @package App\Entities
 */
class Entity
{
    /**
     * Given an array of key/value pairs, will fill in the
     * data on this instance with those values. Only works
     * on fields that exist.
     *
     * @param array $data
     */
    public function fill(array $data)
    {
        foreach ($data as $key => $var)
        {
            $method = 'fill_'. $key;
            if (method_exists($this, $method))
            {
                $this->$method($var);
            }

            elseif (property_exists($this, $key))
            {
                $this->$key = $var;
            }
        }
    }

    //--------------------------------------------------------------------

    //--------------------------------------------------------------------
    // Getters
    //--------------------------------------------------------------------

    /**
     * Returns the created_at field value as a string. If $format is
     * a string it will be used to format the result by. If $format
     * is TRUE, the underlying DateTime option will be returned instead.
     *
     * Either way, the timezone is set to the value of $this->timezone,
     * if set, or to the app's default timezone.
     *
     * @param string $format
     *
     * @return string
     */
    public function created_at($format = 'Y-m-d H:i:s'): string
    {
        $timezone = isset($this->timezone)
            ? $this->timezone
            : app_timezone();

        $this->created_at->setTimezone($timezone);

        return $format === true
            ? $this->created_at
            : $this->created_at->format($format);
    }

    //--------------------------------------------------------------------

    /**
     * Returns the updated_at field value as a string. If $format is
     * a string it will be used to format the result by. If $format
     * is TRUE, the underlying DateTime option will be returned instead.
     *
     * Either way, the timezone is set to the value of $this->timezone,
     * if set, or to the app's default timezone.
     *
     * @param string $format
     *
     * @return string
     */
    public function updated_at($format = 'Y-m-d H:i:s'): string
    {
        $timezone = isset($this->timezone)
            ? $this->timezone
            : app_timezone();

        $this->updated_at->setTimezone($timezone);

        return $format === true
            ? $this->updated_at
            : $this->updated_at->format($format);
    }

    //--------------------------------------------------------------------

    //--------------------------------------------------------------------
    // Setters
    //--------------------------------------------------------------------

    /**
     * Custom value for the `created_at` field used with timestamps.
     *
     * @param string $datetime
     *
     * @return $this
     */
    public function set_created_at(string $datetime)
    {
        $this->created_at = new \DateTime($datetime, new \DateTimeZone('UTC'));

        return $this;
    }

    //--------------------------------------------------------------------

    /**
     * Custom value for the `updated_at` field used with timestamps.
     *
     * @param string $datetime
     *
     * @return $this
     */
    public function set_updated_at(string $datetime)
    {
        $this->updated_at = new \DateTime($datetime, new \DateTimeZone('UTC'));

        return $this;
    }

    //--------------------------------------------------------------------

    //--------------------------------------------------------------------
    // Magic
    //--------------------------------------------------------------------

    /**
     * Allows Models to be able to get any class properties that are
     * stored on this class.
     *
     * For flexibility, child classes can create `get*()` methods
     * that will be used in place of getting the value directly.
     * For example, a `created_at` property would have a `created_at`
     * method.
     *
     * @param string $key
     *
     * @return mixed
     */
    public function __get(string $key)
    {
        // if a set* method exists for this key,
        // use that method to insert this value.
        if (method_exists($this, $key))
        {
            return $this->$key();
        }

        if (isset($this->$key))
        {
            return $this->$key;
        }
    }

    //--------------------------------------------------------------------

    /**
     * Allows Models to be able to set any class properties
     * from the result set.
     *
     * For flexibility, child classes can create `set*()` methods
     * to provide custom setters for keys. For example, a field
     * named `created_at` would have a `set_created_at` method.
     *
     * @param string $key
     * @param null   $value
     */
    public function __set(string $key, $value = null)
    {
        // if a set* method exists for this key,
        // use that method to insert this value.
        $method = 'set_'.$key;
        if (method_exists($this, $method))
        {
            $this->$method($value);
        }

        // A simple insert on existing keys otherwise.
        elseif (isset($this->$key))
        {
            $this->$key = $value;
        }
    }

    //--------------------------------------------------------------------
}

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

Ссылки по теме

Источник: Using Entities in CodeIgniter 4Better Entities in CodeIgniter 4

CodeIgniter Wikipedia: ru.wikipedia.org/wiki/CodeIgniter

Официальный сайт CodeIgniter: www.codeigniter.com

Официальный форум CodeIgniter: forum.codeigniter.com

Repository Design Patterns: https://martinfowler.com/eaaCatalog/repository.html