База данных yii2: Полное руководство (v2): Работа с базами данных

Содержание

Динамические базы данных для ActiveRecord в Yii2

Как-то давно у меня спрашивали, как сделать хранение пользовательского контента в разных базах данных, а недавно этот же вопрос всплыл на форуме вновь:

Подскажите в общих чертах, как можно реализовать динамическое переключение между базами в зависимости от подключенного пользователя. То есть если залогинился пользователь User1, то подключиться к DB1, если User2 то к DB2.

Где такое может быть полезно? Например, если делаете у себя сервис для клиентов, где хотите, чтобы у каждой компании была своя отдельная база данных. Или если делаете мультисайтовость, когда в одной главной панели управляете товарами пяти своих интернет-магазинов. Мультисайтовость для разных хостингов лучше для безопасности и надёжности реализовывать через API, а не через открытие доступа к SQL серверу для всех или для своего главного IP-адреса. Но такие нюансы рассматривать не будем.

Итак, реализуем поддержку нескольких подключений. При работе через свой Data Mapper можно передавать идентификатор пользователя прямо в методы репозитория:

$post = $this->postsRepository->find($id, $userId);
$post->publish();
$this->postsRepository->persist($post, $userId);

и в него вписать всю логику того, какую базу данных для каждого запроса выбирать. Если используете полноценные ORM, то дальше можете статью не читать. Но задача усложняется при использовании ActiveRecord в том же Yii2 тем, что разные подключения можно указать методам запроса one($db) и all($db):

$post = Post::find()->andWhere(['id' => $postId])->one(Yii::$app->db2);

и нельзя передавать другое подключение $db в присутствующие в ActiveRecord методы save, deleteAll и подобные, которые аргумент $db не принимают и полностью полагаются на свой статический метод:

class ActiveRecord extends BaseActiveRecord { public static function getDb() { return Yii::$app->getDb(); } . .. }

и работают внутри только с этим единственным подключением static::getDb().

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

Для демонстрации установим yii2-app-basic приложение и запустим команду:

./yii migrate/create create_post_table --useTablePrefix=1 --fields=title:string
./yii migrate

чтобы создать и запустить миграцию:

use yii\db\Migration;
 
class m160826_073936_create_post_table extends Migration
{
    public function up()
    {
        $this->createTable('{{%post}}', [
            'id' => $this->primaryKey(),
            'title' => $this->string(),
        ]);
    }
 
    public function down()
    {
        $this->dropTable('{{%post}}');
    }
}

И добавим для этой таблицы модель:

namespace app\models;
 
use Yii;
use yii\db\ActiveRecord;
 

@property 
@property 
class Post extends ActiveRecord
{
    public static function tableName()
    {
        return '{{%post}}';
    }
}

Пользовательские посты будем хранить именно в персональных базах для каждого пользователя вроде этого:

'components' => [
    . ..
    'db' => [
        'class' => 'yii\db\Connection',
        'dsn' => 'mysql:host=localhost;dbname=site',
        'username' => 'root',
        'password' => '',
        'charset' => 'utf8',
    ],
    'db_user_1' => [
        'class' => 'yii\db\Connection',
        'dsn' => 'mysql:host=localhost;dbname=user_1',
        'username' => 'root',
        'password' => '',
        'charset' => 'utf8',
    ],
],

Но такой пример с ручным объявлением db_user_1, db_user_2 для тысяч пользователей рассматривать не будем.

Вместо этого можно объявить новое подключение userDb и определить его анонимной функцией:

'components' => [
    ...
    'db' => [
        'class' => 'yii\db\Connection',
        'dsn' => 'mysql:host=localhost;dbname=site',
        'username' => 'root',
        'password' => '',
        'charset' => 'utf8',
    ],
    'userDb' => function () {
        if ($user = Yii::$app->get('user', false)) {
            $userId = !$user->getIsGuest() ? $user->getId() : 0;
        } else {
            $userId = 0;
        }
        return Yii::createObject([
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=user_' .
$userId, 'username' => 'root', 'password' => '', 'charset' => 'utf8', ]); }, ],

и переопределить метод getDb() на использование этого подключения:

class Post extends ActiveRecord
{
    public static function getDb()
    {
        return Yii::$app->userDb;
    }
 
    ...
}

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

Можно накостылить и так:

$oldDb = Yii::$app->db;
$db = Yii::$app->set('db', Yii::$app->userDb);
$post->save();
Yii::$app->set('db', $oldDb);

Но это перебьёт настройки базы всего сайта. При использовании yii\log\DbTarget системные логи запросов внутри исполнения save() могут записаться не в ту базу.

Можно подменять именно userDb вместо общей базы:

$oldDb = Yii::$app->userDb;
$db = Yii::$app->set('userDb', Yii::$app->userDb143);
$post->save();
Yii::$app->set('userDb', $oldDb);

Или для изменения результата статического getDb() можно добавить специальное поле:

class Post extends ActiveRecord
{
    public static $db;
 
    public static function getDb()
    {
        return self::$db;
    }
}

и присваивать туда нужное подключение:

Post::$db = Yii::$app->userDb143;
$post->save();

Но как при этом создавать свои подключения вроде Yii::$app->userDb143 для самих пользователей?

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

Реализация UserDbLocator

Просто объявить 'userDb' => function () с возвратом разных результатов для каждого запроса в components мы не можем, так как в ServiceLocator все объекты создаются только один раз и для следующих вызовов кешируются.

Вместо дёргания самих подключений из Yii::$app удобнее зарегистрировать компонент-фабрику для создания и хранения этих подключений.

Например, создать UserDbLocator с указанием ему шаблона для генерации каждого экземпляра и зарегистрировать его так:

'components' => [
    'db' => [...],
    'userDbLocator' => [
        'class' => 'app\components\UserDbLocator',
        'connection' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=user_{id}',
            'username' => 'user_{id}',
            'password' => 'xxx',
            'charset' => 'utf8',
        ],
    ],
],

И он уже на основе ID залогиненного пользователя будет возвращать нужное соединение.

И именно его мы будем использовать в своём коде как любой другой компонент приложения:

$userDb = Yii::$app->userDbLocator->getDb();

и его же впишем в статическом методе getDb() наших сущностей:

class Post extends ActiveRecord
{
    public static function getDb()
    {
        return Yii::$app->get('userDbLocator')->getDb();
    }
}

А для администратора сайта при необходимости можно организовать переключение

$userId методом switchId():

class PostController extends Controller
{
    public function actionUpdate($user_id, $id)
    {
        Yii::$app->userDbLocator->switchId($user_id);
        $model = $this->findModel($Id);
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'user_id' => $user_id, 'id' => $model->id]);
        }
        return $this->render('update', ['model' => $model]);
    }
}

С требованиями определились. Реализовать это можно примерно так:

namespace app\components;
 
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
 
class UserDbLocator extends Component
{
    public $connection;
    public $component = 'db_{id}';
    public $defaultId = 0;
 
    private $activeId;
 
    public function init()
    {
        if (!is_array($this->connection)) {
            throw new InvalidConfigException('User connection must be set as an array.');
        }
        parent::init();
    }
 
    public function getDb()
    {
        $dbId = $this->getDbId();
        if (!Yii::$app->has($dbId)) {
            Yii::$app->set($dbId, $this->buildConnection());
        }
        return Yii::$app->get($dbId);
    }
 
    public function switchId($id)
    {
        return $this->activeId = $id;
    }
 
    private function getDbId()
    {
        return $this->replacePlaceholder($this->component);
    }
 
    private function buildConnection()
    {
       return array_map([$this, 'replacePlaceholder'], $this->connection);
    }
 
    private function replacePlaceholder($value)
    {
        return str_replace('{id}', $this->getActiveId(), $value);
    }
 
    private function getActiveId()
    {
        if ($this->activeId === null) {
            $user = Yii::$app->get('user', false);
            if ($user && !$user->getIsGuest()) {
                $this->activeId = $user->getId();
            } else {
                $this->activeId = $this->defaultId;
            }
        }
        return $this->activeId;
    }
}

Здесь в методе getDb() мы генерируем название компонента и его определение, заменяя везде {id} на ID выбранного или текущего пользователя. И, чтобы не возиться с созданием и кешированием объекта вручную, помещаем его в ServiceLocator через Yii::$app->set(...).

По умолчанию getActiveId() будет возвращать ID залогиненного пользователя. Но администратору можно будет переключить его вручную через

switchId($userId) и вернуть назад с помощью switchId(null):

Yii::$app->userDbLocator->switchId($otherUserId);
$post->save();
Yii::$app->userDbLocator->switchId(null);

Далее мы упростим этот вызов, а пока…

Обобщим до DynamicLocator

Провайдер UserDbLocator получился негибкий, так как умеет получать только подключения к базе:

Yii::$app->get('userDbLocator')->getDb();

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

Для этого можно переименовать его в DynamicLocator и его поле connection переименовать в template.

И ещё можно добавить возможность определения шаблонов в виде анонимных функций, принимающих активный $userId:

'components' => [
    'userDbLocator' => [
        'class' => 'app\components\DynamicLocator',
        'component' => 'db_user_{id}',
        'template' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=user_{id}',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8',
        ],
    ],
    'userCacheLocator' => [
        'class' => 'app\components\DynamicLocator',
        'component' => 'cache_user_{id}',
        'template' => function ($userId) {
            ...
        },
    ],
],

А сам метод getDb() переименовать в get() для получения инстанса своего компонента:

Yii::$app->get('userDbLocator')->get();
Yii::$app->get('userCacheLocator')->get();

И ещё неплохо бы помимо предыдущего переключения активного пользователя администратором:

Yii::$app->userDbLocator->switchId($userId);
Yii::$app->userDbLocator->get();

добавить явную передачу $userId в метод get:

Yii::$app->userDbLocator->get();
Yii::$app->userDbLocator->get($userId);

Для этого всего немного перепишем компонент:

namespace app\components;
 
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\di\Instance;
 
class DynamicLocator extends Component
{
    public $component;
    public $template;
    public $defaultId = 0;
 
    public $activeId;
 
    public function init()
    {
        if (empty($this->component)) {
            throw new InvalidConfigException('Component must be set. ');
        }
        if (empty($this->template)) {
            throw new InvalidConfigException('Template must be set.');
        }
        parent::init();
    }
 
    public function get($id = null)
    {
        $componentId = $this->getComponentId($id);
        if (!Yii::$app->has($componentId)) {
            Yii::$app->set($componentId, $this->buildComponent($id));
        }
        return Yii::$app->get($componentId);
    }
 
    public function switchId($id)
    {
        return $this->activeId = $id;
    }
 
    private function getComponentId($id)
    {
        return call_user_func($this->replacer($id), $this->component);
    }
 
    private function buildComponent($id)
    {
        if (is_array($this->template)) {
            return array_map($this->replacer($id), $this->template);
        } else {
            return call_user_func($this->template, $id);
        }
    }
 
    private function replacer($id)
    {
        return function ($value) use ($id) {
            return str_replace('{id}', $this->getActiveId($id), $value);
        };
    }
 
    private function getActiveId($id)
    {
        if ($id !== null ) {
            return $id;
        }
        if ($this->activeId === null) {
            $user = Yii::$app->get('user', false);
            if ($user && !$user->getIsGuest()) {
                $this->activeId = $user->getId();
            } else {
                $this->activeId = $this->defaultId;
            }
        }
        return $this->activeId;
    }
}

Помимо переименования переменных и методов мы дополнили метод buildComponent() поддержкой анонимных функций.

Теперь конфигурируем:

'components' => [
    'userDbLocator' => [
        'class' => 'app\components\DynamicLocator',
        'component' => 'db_user_{id}',
        'template' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=user_{id}',
            'username' => 'user_{id}',
            'password' => '',
            'charset' => 'utf8',
        ],
    ],
],

и используем в Post:

class Post extends ActiveRecord
{
    public static function getDb()
    {
        return Yii::$app->get('userDbLocator')->get();
    }
}

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

Обобщим до DynamicServiceLocator

В предыдущем варианте наш провайдер работал только с одним компонентом. Мы подключали несколько копий провайдера с разными настройками в template.

Вместо этого можно сделать один DynamicServiceLocator, работающий с несколькими компонентами сразу.

Имеющиеся варианты вызова от разных источников:

Yii::$app->get('userDbLocator')->get();
Yii::$app->get('userCacheLocator')->get();

можно преобразовать в обобщённый вызов по имени компонента от одного источника:

Yii::$app->get('dynamicLocator')->get('db');
Yii::$app->get('dynamicLocator')->get('log');

То есть по поведению он должен напоминать стандартный класс yii\di\ServiceLocator, объектом которого является сам $app, компоненты которого мы дергаем через Yii::$app->get('db').

Также можно добавить возможность ручного указания ID:

$userDb = Yii::$app->dynamicLocator->get('db', $userId);

и оставить предыдущий вариант для переключения активного ID администратором:

Yii::$app->dynamicLocator->switchId($userId);
...
Yii::$app->dynamicLocator->switchId(null);

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

'components' => [
    'dynamicLocator' => [
        'class' => 'app\components\DynamicServiceLocator',
        'components' => [
            'db' => [
                'class' => 'yii\db\Connection',
                'dsn' => 'mysql:host=localhost;dbname=user_{id}',
                'username' => 'root',
                'password' => 'root',
                'charset' => 'utf8',
            ],
            'cache' => function ($id) {
                . ..
            }
        ],
    ],
],

Теперь немного переработаем исходный код.

Наш прошлый код работал с компонентами и обрабатывал ID активного пользователя прямо в методе getActiveId():

class DynamicLocator
{
    ...
 
    private function getActiveId($id)
    {
        if ($id !== null ) {
            return $id;
        }
        if ($this->activeId === null) {
            $user = Yii::$app->get('user', false);
            if ($user && !$user->getIsGuest()) {
                $this->activeId = $user->getId();
            } else {
                $this->activeId = $this->defaultId;
            }
        }
        return $this->activeId;
    }
}

Если работа с компонентами неизменна и универсальна, то работа с пользователем может поменяться. Это может понадобиться, например, если в каком-то проекте отдельная база создаётся для компании, а не для каждого сотрудника. В этом случае строку:

$this->activeId = $user->getId();

придётся переписать на более продвинутую:

$this->activeId = $user->identity->company_id;

Как предоставить возможность переписывать данный фрагмент в разных проектах?

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

Во-первых, можно использовать наследование. Для этого можно вместо private обозначить метод getActiveId как protected, чтобы в другом проекте можно было отнаследоваться с переопределением метода:

class MyDynamicLocator extends DynamicLocator
{
    protected function getActiveId() {
        ...
    }
}

и сменить класс в конфигурации:

'components' => [
    'dynamicLocator' => [
        'class' => 'app\components\MyDynamicLocator',
]

Но тогда и поле activeId нужно обозначать как protected. Да и весь метод:

protected function getActiveId($id)
{
    if ($id !== null ) {
        return $id;
    }
    if ($this->activeId === null) {
        ...
        else {
            $this->activeId = $this->defaultId;
        }
    }
    return $this->activeId;
}

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

Наследование предоставляется простой сменой модификаторой доступа всех изменяемых внутренностей класса с private на protected. Делается быстро, но пользоваться этим неудобно, так как надо помнить, какие переменные вроде activeIdи defaultId в базовом классе есть и для чего они нужны. И при переименовании этого всего в базовом классе нужно переписывать и всех наследников.

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

Можно эту строку вынести в какой-либо шаблонный protected-метод, который вызывать как $this->getOriginalActiveId($this->default) и переопределять в наследниках. Так уже будет удобнее.

Во-вторых, вместо наследования можно воспользоваться композицией. А именно, просто вынести изменяемый код метода getActiveId() и поле default в отдельный объект. И тогда этот код основного компонента:

class DynamicLocator extends Component
{
    public $defaultId;
 
    private $activeId;
 
    . ..
 
    public function switchId($id)
    {
        return $this->activeId = $id;
    }
 
    private function getActiveId($id)
    {
        if ($id !== null ) {
            return $id;
        }
        if ($this->activeId === null) {
            $user = Yii::$app->get('user', false);
            if ($user && !$user->getIsGuest()) {
                $this->activeId = $user->getId();
            } else {
                $this->activeId = $this->defaultId;
            }
        }
        return $this->activeId;
    }
}

перепишется на такой:

class DynamicLocator extends Component
{
    ...
 
    public function switchId($id)
    {
        $this->forcedId = $id;
    }
 
    private function getCurrentId($id)
    {
        if ($id !== null ) {
            return $id;
        }
        if ($this->forcedId !== null ) {
            return $this->forcedId;
        }
        return $this->activeId->get();
    }
}

А от объекта activeId в данном случае понядобится только наличие метода get(). Этот факт можно обозначить интерфейсом:

interface ActiveIdInterface
{
    public function get();
}

Теперь достаточно будет передать любой класс, который будет реализовывать данный интерфейс. В стиле Yii2 он может выглядеть так:

class ActiveUserId extends Object implements ActiveIdInterface
{
    public $default = '';
 
    public function get()
    {
        $user = Yii::$app->get('user', false);
        if ($user && !$user->getIsGuest()) {
            return $user->getId();
        }
        return $this->default;
    }
}

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

class ActiveUserId implements ActiveIdInterface
{
    private $user;
    private $default;
 
    public function __construct(\yii\web\User $user = null, $default = '')
    {
        $this->user = $user;
        $this->default = $default;
    }
 
    public function get()
    {
        return $this->user && !$this->user->getIsGuest() ? $user->getId() : $this->default;
    }
}

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

Теперь мы сможем получать подключение для текущего пользователя внутри ActiveRecord:

class Post extends ActiveRecord
{
    public static function getDb()
    {
        return Yii::$app->get('dynamicLocator')->get('db');
    }
}

или для текущего или любого другого пользователя снаружи:

$currentUserDb = Yii::$app->dynamicLocator->get('db');
$anotherUserDb = Yii::$app->dynamicLocator->get('db', $userId);

И можем в нужный момент переключать текущего пользователя:

Yii::$app->dynamicLocator->switchId($otherUserId);
$post->save();
Yii::$app->dynamicLocator->switchId(null);

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

Но в этом сейчас имеется несколько проблем. Они усугубляется при необходимости делать, например, вложенные операции. При этом нужно будет запомнить старый $oldId до операции и вернуть его на место после:

$oldId = $dynamicLocator->getActiveId();
$dynamicLocator->switchId($otherUserId);
$post->save();
$dynamicLocator->switchId($oldId);

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

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

Третья – нарушение инкапсуляции. Теперь метод getActiveId() нужно делать публичным, чтобы все его могли дёргать для запоминания старого состояния.

Четвёртая – наличие глобальных переменных:

У нашего компонента имеется публичный метод switchId($id), позволяющий переключать внутреннее состояние компонента любому наружному коду. Такая общедоступная «глобальная переменная» может привести к проблеме любой общедоступности: в какой-то момент станет не очень понятно, кто и когда её переключил и кто забыл её «положить на место».

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

Но как быть, если всё-таки переключать нужно? В функциональном подходе это реализуется с помощью неизменяемых (Immutable) объектов. Для этого в методах set() и switchId() вместо изменения поля мы просто создаём новый объект-клон с другими $components и $id:

class DynamicLocator
{    
    private $components = [];
    private $activeId;
    private $forcedId;
 
    public function __construct(array $components, $forcedId, ActiveIdInterface $activeId)
    {
        $this->components = $components;
        $this->forcedId = $forcedId;
        $this->activeId = $activeId;
    }
 
    public function set($componentId, $definition)
    {
        $newComponents = array_merge($this->components, [$componentId => $definition]);
        return new self($newComponents, $this->activeId);
    }
 
    public function switchId($id)
    {
        return new self($this->components, $id, $this->activeId);
    }
 
    . ..
}

Поля объекта задаются при конструировании и больше никогда не меняются. Вместо смены поля здесь через new self() создаётся новый объект с новыми данными. Теперь если разные модули приложения где-то у себя пробуют менять идентификатор, они получают свою копию и никак не влияют на другие модули, использующие свои отдельные клоны:

$locator = new DynamicLocator([], new ActiveUserId(Yii::$app->user, 0));
 
$locator2 = $Locator->set('db', [
    'username' => 'user_{id}',
]);
 
$locator3 = $Locator2->switchId(5);
 
echo $locator2->db->username; 
echo $locator3->db->username; 

Так можно наплодить нужное количество независимых друг от друга компонентов. Но зачем?

В обычном коде – незачем. Но если попробуете реализовать честную многопоточность в PHP7 с pthreads, где в консольном контроллере будете в нескольких потоках вызывать одновременно switchId() у одного и того же Yii::$app->locator, то увидите удивительную путаницу. Вместо этого можно либо каждому потоку дать свой независимый клон $locator, либо, что более корректно, избавиться от хранения состояния c «записывающим» методом switchId(), пользуясь только «читающим» методом get('db', $userId) для извлечения нужного подключения.

Итак, в функциональном подходе это реализуется доступом ко всему только на чтение, а с ActiveRecord такая многопоточность не реализуется. Мы не можем хранить несколько объектов подключения внутри разных извлечённых экземпляров $post, чтобы два вызова метода save():

$post1 = Post::find()->andWhere(['id' => 5])->one($db1);
$post2 = Post::find()->andWhere(['id' => 5])->one($db2);
 
$post1->save();
$post2->save();

записали объект в разные базы, так как $post1 и $post2 при сохранении дёргают один и тот же статический static::getDb():

class ActiveRecord extends BaseActiveRecord
{
    public static function getDb()
    {
        return Yii::$app->db;
    }
    . ..
}

а всё статическое всегда имеется в общем единственном экземпляре. Именно при попытках сделать несколько вариаций одного и того же понимаешь, что синглтоны – зло.

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

$db1 = $dynamicLocator->get('db', $userId1);
$post1 = Post::find()->andWhere(['id' => 5])->one($db1);
...
$oldId = $dynamicLocator->getActiveId();
$dynamicLocator->switchId($userId1);
$post1->save();
$dynamicLocator->switchId($oldId);

Вторую такую конструкцию, как мы уже говорили, использовать неудобно. Можно ли все эти действия упростить и выполнять одним вызовом? И как избежать опечаток и забывчивости программиста?

Это всё можно упростить обрамлением нашего кода в блок:

$post = $dynamicLocator->doWith($userId, function () {
    return Post::findOne(5);
});
 
...
 
$dynamicLocator->doWith($userId, function () use ($post) {
    $post->save();
});

Пусть метод doWith() внутри себя переключает идентификатор, выполняет нашу функцию и после выполнения возвращает всё обратно:

public function doWith($id, callable $function)
{
    $oldId = $this->getCurrentId(null);
    $this->forcedId = $id;
    $result = call_user_func($function, $this);
    $this->forcedId = $oldId;
    return $result;
}

Это удобнее использовать и меньше вероятность что-то перепутать. И при этом можно удалить метод switchId, а метод getActiveId сделать приватным.

У нас не осталось глобального изменяемого снаружи через switchId($id) состояния внутри компонента. Мы можем не переживать, что кто-то его нечаянно изменит значение во вложенном коде и забудет вернуть его назад.

Для многопоточности такой хак не подойдёт, так как объект внутри всё-таки меняется при вызове doWork() и каждый поток будет мешать соседям. Но для однопоточного исполнения мы осуществили полную эмуляцию в рамках синглтонного статического метода getDb() в ActiveRecord.

Приступим теперь к реализации самого провайдера.

Похожесть со стандартным фреймворковским ServiceLocator даёт нам возможность использовать прямо этот класс как базу для нашего компонента. Отнаследуемся от него и переопределим методы get() и clear():

namespace app\components;
 
use Yii;
use yii\di\Instance;
use yii\di\ServiceLocator;
 
class DynamicServiceLocator extends ServiceLocator
{
     @var 
    public $activeId;
 
    private $forcedId;
 
    public function init()
    {
        $this->activeId = Instance::ensure($this->activeId, 'app\components\ActiveIdInterface');
        parent::init();
    }
 
    public function get($componentId, $id = null, $throwException = true)
    {
        $serviceId = $this->generateServiceId($componentId, $id);
        if (!parent::has($serviceId)) {
            parent::set($serviceId, $this->buildDefinition($componentId, $id));
        }
        return parent::get($serviceId, $throwException);
    }
 
    public function clear($componentId, $id)
    {
        parent::clear($componentId);
        parent::clear($this->generateServiceId($componentId, $id));
    }
 
    public function doWith($id, callable $function)
    {
        $oldId = $this->getCurrentId(null);
        $this->forcedId = $id;
        $result = call_user_func($function, $this);
        $this->forcedId = $oldId;
        return $result;
    }
 
    private function generateServiceId($componentId, $id)
    {
        return 'dynamic_' .  $componentId . '_' . $this->getCurrentId($id);
    }
 
    private function buildDefinition($componentId, $id)
    {
        $definitions = $this->getComponents();
        if (!array_key_exists($componentId, $definitions)) {
            return null;
        };
        $definition = $definitions[$componentId];
        if (is_array($definition)) {
            $currentId = $this->getCurrentId($id);
            array_walk_recursive(
                $definition,
                function (&$value) use ($currentId) {
                    if (is_string($value)) {
                        $value = str_replace('{id}', $currentId, $value);
                    }
                }
            );
            return $definition;
        } elseif (is_object($definition) && $definition instanceof \Closure) {
            return Yii::$container->invoke($definition, [$this->getCurrentId($id)]);
        } else {
            return $definition;
        }
    }
 
    private function getCurrentId($id)
    {
        if (!empty($id)) {
            return $id;
        }
        if (!empty($this->forcedId)) {
            return $this->forcedId;
        }
        return $this->activeId->get();
    }
}

Внутри метода buildDefinition() мы не можем обращаться напрямую к определениям приватного массива родительского класса вроде $this->_components[$id], поэтому воспользовались вызовом метода getComponents(). И здесь мы реализовали полный обход определений из components через array_walk_recursive, чтобы уметь производить замену {id} даже во вложенных массивах.

Далее нам нужно приложить интерфейс детектора активного пользователя:

namespace app\components;
 
interface ActiveIdInterface
{
    public function get();
}

и его простейшую реализацию в рамках Yii2:

namespace app\components;
 
use Yii;
use yii\base\Object;
 
class ActiveUserId extends Object implements ActiveIdInterface
{
    public $default = '';
 
    public function get()
    {
        $user = Yii::$app->get('user', false);
        if ($user && !$user->getIsGuest()) {
            return $user->getId();
        }
        return $this->default;
    }
}

Далее подключаем наш провайдер в конфигурационном файле:

'components' => [
    'user' => [...],
    'db' => [...],
    ...
    'dynamicLocator' => [
        'class' => 'app\components\DynamicServiceLocator',
        'activeId' => [
            'class' => 'app\components\ActiveUserId',
            'default' => 0,
        ],
        'components' => [
            'db' => [
                'class' => 'yii\db\Connection',
                'dsn' => 'mysql:host=localhost;dbname=user_{id}',
                'username' => 'root',
                'password' => 'root',
                'charset' => 'utf8',
            ],
            'cache' => function ($id) {
                
            }
        ],
    ],
],

и прописываем в классе Post:

class Post extends ActiveRecord
{
    public static function getDb()
    {
        return Yii::$app->get('dynamicLocator')->get('db');
    }
}

Теперь многопользовательский контроллер администратора можно переделать под вывод и редактирование записей любого выбранного пользователя (а не только собственные) простым обрамлением любого участка кода конструкцией doWith:

class PostController extends Controller
{
    public function actionIndex($user_id = null)
    {
        return Yii::$app->dynamicLocator->doWith($user_id, function () {
            $dataProvider = new ActiveDataProvider([
                'query' => Post::find()->with('category'),
            ]);
            return $this->render('index', [
                'dataProvider' => $dataProvider,
            ]);
        }
    }
 
    public function actionCreate($id, $user_id = null)
    {
        return Yii::$app->dynamicLocator->doWith($user_id, function () use ($id) {
            $model = new Post();
            if ($model->load(Yii::$app->request->post()) && $model->save()) {
                return $this->redirect(['view', 'id' => $model->id]);
            }
            return $this->render('create', [
                'model' => $model,
            ]); 
        }
    }
}

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

Web-разработка • Yii2 и Laravel

Обычно для записи данных в БД используется метод save(), также можно использовать метод update(). Но метод save() используется чаще, потому что он более универсален. С ним можно как вставлять новые данныеб так и обновлять уже существующие данные. То есть, можно выполнять как запрос INSERT, так и запрос UPDATE.

У нас уже есть форма обратной связи, которую мы создали, когда работали с классом ActiveForm:

<?php
namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\FeedbackForm;

class PageController extends Controller {
    public function actionIndex() {
        return $this->render('index');
    }

    public function actionFeedback()
    {
        $model = new FeedbackForm();
        // если пришли post-данные...
        if ($model->load(Yii::$app->request->post())) {
            // . ..проверяем эти данные
            if ($model->validate()) {
                // данные прошли валидацию, отмечаем этот факт
                Yii::$app->session->setFlash(
                    'success',
                    true
                );
                // перезагружаем страницу, чтобы избежать повтороной отправки формы
                return $this->refresh();
            } else {
                // данные не прошли валидацию, отмечаем этот факт
                Yii::$app->session->setFlash(
                    'success',
                    false
                );
                // не перезагружаем страницу, чтобы сохранить пользовательские данные
            }
        }
        return $this->render('feedback', ['model' => $model]);
    }
}
<?php
namespace app\models;

use yii\base\Model;

/**
 * Модель для формы обратной связи
 */
class FeedbackForm extends Model
{
    public $name, $email, $body;

    public function attributeLabels() {
        return [
            'name' => 'Ваше имя',
            'email' => 'Ваш email',
            'body' => 'Сообщение',
        ];
    }

    public function rules() {
        return [
            // удалить пробелы для полей name и email
            [['name', 'email'], 'trim'],
            // поле name обязательно для заполнения
            ['name', 'required', 'message' => 'Поле «Ваше имя» обязательно для заполнения'],
            // поле email обязательно для заполнения
            ['email', 'required', 'message' => 'Поле «Ваш email» обязательно для заполнения'],
            // поле email должно быть корректным адресом почты
            ['email', 'email', 'message' => 'Поле «Ваш email» должно быть адресом почты'],
            // поле body обязательно для заполнения
            ['body', 'required', 'message' => 'Поле «Сообщение» обязательно для заполнения'],
        ];
    }
}
<?php
/* @var $this yii\web\View */

use yii\widgets\ActiveForm;
use yii\helpers\Html;

$this->title = 'Обратная связь';
?>

<?php if (Yii::$app->session->hasFlash('success')): ?>
    <?php if (Yii::$app->session->getFlash('success')): ?>
        <p>Данные формы прошли валидацию</p>
    <?php else: ?>
        <p>Данные формы не прошли валидацию</p>
    <?php endif; ?>
<?php endif; ?>

<div>
    <h2><?= Html::encode($this->title) ?></h2>

    <?php $form = ActiveForm::begin(['id' => 'feedback-form', 'options' => ['novalidate' => '']]); ?>
        <?= $form->field($model, 'name')->textInput(); ?>
        <?= $form->field($model, 'email')->textInput(); ?>
        <?= $form->field($model, 'body')->textarea(['rows' => 5]); ?>
        <?= Html::submitButton('Отправить', ['class' => 'btn btn-primary']); ?>
    <?php ActiveForm::end(); ?>
</div>

Создадим таблицу feedback в базе данных, чтобы сохранять сообщения:

CREATE TABLE `feedback` (
  `id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Уникальный идентификатор',
  `name` varchar(100) NOT NULL COMMENT 'Имя автора сообщения',
  `email` varchar(100) NOT NULL COMMENT 'Почта автора сообщения',
  `body` text NOT NULL COMMENT 'Текст сообщения'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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

<?php
namespace app\models;

use yii\db\ActiveRecord;

/**
 * Модель для формы обратной связи
 */
class Feedback extends ActiveRecord
{
    public function attributeLabels() {
        return [
            'name' => 'Ваше имя',
            'email' => 'Ваш email',
            'body' => 'Сообщение',
        ];
    }

    public function rules() {
        return [
            // удалить пробелы для полей name и email
            [['name', 'email'], 'trim'],
            // поле name обязательно для заполнения
            ['name', 'required', 'message' => 'Поле «Ваше имя» обязательно для заполнения'],
            // поле email обязательно для заполнения
            ['email', 'required', 'message' => 'Поле «Ваш email» обязательно для заполнения'],
            // поле email должно быть корректным адресом почты
            ['email', 'email', 'message' => 'Поле «Ваш email» должно быть адресом почты'],
            // поле body обязательно для заполнения
            ['body', 'required', 'message' => 'Поле «Сообщение» обязательно для заполнения'],
        ];
    }
}
<?php
namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\Feedback;

class PageController extends Controller {
    public function actionIndex() {
        return $this->render('index');
    }

    public function actionFeedback()
    {
        $model = new Feedback();
        // если пришли post-данные. ..
        if ($model->load(Yii::$app->request->post())) {
            // ...проверяем и сохраняем эти данные
            if ($model->save()) {
                // данные прошли валидацию, отмечаем этот факт
                Yii::$app->session->setFlash(
                    'success',
                    true
                );
                // перезагружаем страницу, чтобы избежать повтороной отправки формы
                return $this->refresh();
            } else {
                // данные не прошли валидацию, отмечаем этот факт
                Yii::$app->session->setFlash(
                    'success',
                    false
                );
                // не перезагружаем страницу, чтобы сохранить пользовательские данные
            }
        }
        return $this->render('feedback', ['model' => $model]);
    }
}
<?php
/* @var $this yii\web\View */

use yii\widgets\ActiveForm;
use yii\helpers\Html;

$this->title = 'Обратная связь';
?>

<?php if (Yii::$app->session->hasFlash('success')): ?>
    <?php if (Yii::$app->session->getFlash('success')): ?>
        <p>Данные формы записаны в базу данных</p>
    <?php else: ?>
        <p>Данные формы не прошли валидацию</p>
    <?php endif; ?>
<?php endif; ?>

<div>
    <h2><?= Html::encode($this->title) ?></h2>

    <?php $form = ActiveForm::begin(['id' => 'feedback-form', 'options' => ['novalidate' => '']]); ?>
        <?= $form->field($model, 'name')->textInput(); ?>
        <?= $form->field($model, 'email')->textInput(); ?>
        <?= $form->field($model, 'body')->textarea(['rows' => 5]); ?>
        <?= Html::submitButton('Отправить', ['class' => 'btn btn-primary']); ?>
    <?php ActiveForm::end(); ?>
</div>

Мы изменили имя класса модели на Feedback (по имени таблицы БД) и теперь класс наследует ActiveRecord. Нам больше не нужно объявлять свойства класса, потому что Yii сам их объявит, выполнив служебный запрос к таблице БД feedback:

SHOW FULL COLUMNS FROM `feedback`

Метод save() не только сохраняет данные, но и проверяет их. Если данные не прошли валидацию, метод возвращает false и запись в БД не происходит.

Поиск: PHP • POST • Web-разработка • Yii2 • База данных • Запрос • Фреймворк • save • UPDATE • INSERT • ActiveRecord • ActiveForm

Как настроить подключение к базе данных в PHP фреймворке Yii2

Простая и тривиальная задача с которой может столкнуться начинающий Yii2 разработчик. Начнем с того, что в зависимости от конфигурации проекта, БД может настраиваться по разному. Возьмем два варианта, которые предоставляет нам см фреймворк:

  1. basic (простой вариант)
  2. advanced (продвинутый вариант)

В обоих из вариантов, настройка БД (указание префикса/tablePrefix, имя/username, пароля/password, dsn и прочего) выполняется одинаково. Все зависит от месторасположения файла и «коннектор» класса работы с БД. В нашем примере мы будем рассматривать вариант взаимодействия с базой данных MySQL, как с одной из самых популярных.

Если брать basic, то конфигурационный файл подключения, можно найти по адресу — «/config/db.php».

С advanced вариантом, немного посложнее (на то он и продвинутый, т. е. для крупных проектов). Здесь может быть более одного файла настроек БД (для разделов — backend, frontend, пр.). Все зависит от того, как изначально вы сконфигурировали свое приложение. Так как админка и пользовательская часть зачастую взаимодействуют с одной базой, то файл настроек я обычно помещаю в каталог — «/common/config/db.php». А в конфиге «/common/config/main.php», прописываю доступ к выше упомянутому. Таким образом и в backend, и в frontend, подключение к БД автоматически «подтянется».


В обеих вариантах настройки идентичны и выглядят следующим образом:


return [
	'class' => 'yiidbConnection',
	'dsn' => 'mysql:host=127. 0.0.1;dbname=my_db_name',
	'username' => 'root',
	'password' => '',
	'charset' => 'utf8',
	'tablePrefix' => 'my_',
];

Где:
class — класс-коннектор подключения к БД
dsn — хост (host) и название БД (dbname)
username — пользоватеь БД
password — пароль пользователя
charset — кодировка БД
tablePrefix — префикс таблиц (т. е. yii2 table prefix)

Вот и все, не так уж и сложно. Успехов!

Yii 2. Вывести записи из базы данных (CRUD)

Настройки базы данных указаны в файле config/db.php.


# config/db.php

return [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=localhost;dbname=database_name',
    'username' => 'root',
    'password' => 'password',
    'charset' => 'utf8',
];

Компонент Gii доступен по умолчанию через URL /gii или /index. php?r=gii (если не настроен ЧПУ).

В итоге должна открыться следующая страница:

Если Gii не выводится, надо убедиться, что в файле config/web.php есть следующий код:


# config/web.php

if (YII_ENV_DEV) {
    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = [
        'class' => 'yii\gii\Module',
        'allowedIPs' => ['127.0.0.1', '::1']m
    ];
}

В Gii надо войти на страницу Model Generator и в поле Table Name ввести имя таблицы, с которой надо выводить записи, например «page» (данная таблица должа быть предварительно создана в базе данных).

Затем нажать Preview, отметить файл в таблице «Code File» и нажать на Generate.

В результате появится файл models/Page.php, который связывает таблицу «page» и сайт на Yii 2.

На странице CRUD Generator надо указать параметры, как показано на картинке ниже:

И нажать на «Preview» и затем на «Generate».

В результате будут созданы файлы, а записи из таблицы «page» будут выводиться на странице /page или /index.php?r=page. При этом записи можно открывать на отдельных страницах, добавлять новые, обновлять и удалять (показано на картинке ниже).

PHP: Постоянные соединения с базами данных

Постоянные соединения представляют собой связи с базами данных, которые не закрываются при завершении скрипта. При получении запроса на постоянное соединение PHP вначале проверяет, имеется ли идентичное постоянное соединение (которое было открыто при предыдущих обращениях) и, если таковое было найдено, использует его. В случае, если идентичного соединения нет, PHP создаёт новое. Под «идентичным» подразумевается соединение, открытое на том же хосте с таким же именем пользователя и паролем (если они указаны).

Та часть разработчиков, которая не имеет чёткого представления о том, как работает веб-сервер и как распределяется нагрузка, могут получить ошибочное представление о том, чем на самом деле являются постоянные соединения. В частности, постоянные соединения не предоставляют возможность открывать ‘пользовательские сессии’ в том же самом соединении, они не предоставляют возможность организовывать более эффективные транзакции, также они не предоставляют множества других полезных возможностей. Фактически, постоянные соединения не предоставляют никакой функциональности, которая была бы невозможна в непостоянных аналогичных соединениях.

Почему?

Это зависит от того, как происходит взаимодействие с веб-сервером. Существует три основных способа использования PHP сервером для генерации веб-страниц.

Первый способ заключается в том, чтобы использовать PHP как CGI-обёртку. При этом PHP-интерпретатор создаётся и уничтожается при каждом обращении к странице (PHP-скрипту). Поскольку интерпретатор уничтожается после каждого запроса к серверу, все используемые им ресурсы (в том числе и соединение с базой данных) закрываются. Следовательно, в этом случае вы не получите ничего от использования постоянных соединений — их просто нет.

Второй, и наиболее популярный способ — использовать PHP как модуль в сервере, который использует несколько процессов. В число таких серверов сейчас входит только Apache. В таком случае, можно выделить один процесс (родительский), который координирует работу всех остальных процессов (дочерних), которые фактически и выполняют работу по обслуживанию веб-страниц. При каждом обращении клиента к серверу запрос перенаправляется одному из дочерних процессов, который в данный момент не занят обслуживанием другого клиента. Это означает, что когда тот же самый клиент выполняет повторный запрос к серверу, он может быть обработан другим дочерним процессом, отличным от того, который был при первом обращении. После открытия постоянного соединения каждая последующая страница, требующая соединения с базой данных, может использовать уже установленное ранее соединение с SQL-сервером.

Третий способ — использовать PHP в качестве плагина в многопоточном веб-сервере. В настоящее время в PHP реализована поддержка ISAPI, WSAPI, и NSAPI (для Windows-платформ), которые позволяют подключать PHP к таким многопоточным серверам, как Netscape FastTrack (iPlanet), Microsoft’s Internet Information Server (IIS) и O’Reilly WebSite Pro. В этом случае поведение PHP полностью аналогично рассмотренной ранее модели с использованием нескольких процессов.

Если постоянные соединения не предоставляют никакой дополнительной функциональности, чем же они тогда так хороши?

Ответ содержится в повышении эффективности. Постоянные соединения полезны в том случае, если при открытии большого количества SQL-соединений возникает ощутимая нагрузка на сервер. То, насколько велика эта нагрузка, зависит от многих факторов. Например, от того, какая именно база данных используется, находится ли она на том же компьютере что и ваш веб-сервер, насколько загружена машина, на которой установлен SQL-сервер, и так далее. В случае, если затраты на установку соединения велики, постоянные соединения могут вам существенно помочь. Они позволяют дочернему процессу на протяжении всего жизненного цикла использовать одно и то же соединение вместо того, чтобы создавать его при обработке каждой страницы, которая взаимодействует с SQL-сервером. Это означает, что каждый дочерний процесс, открывший постоянное соединение, будет иметь своё собственное соединение с сервером. Например, если у вас запущено 20 дочерних процессов, которые выполнили скрипт, использовавший постоянное соединение с SQL-сервером, вы получите 20 различных соединений с SQL-сервером, по одному на каждый дочерний процесс.

Следует заметить, что этот подход имеет некоторые недостатки: если вы используете базу данных с ограниченным количеством возможных подключений, оно может быть превышено количеством запрашиваемых дочерними процессами постоянных соединений. Например, если ваша база данных позволяет 16 одновременных соединений, и во время нагрузки на сервер 17 дочерних процессов попробуют открыть соединение, одна из попыток потерпит неудачу. Если в вашем коде содержатся ошибки, не позволяющие закрывать соединение (например, бесконечные циклы), база данных с 32 одновременными подключениями вскоре может оказаться заблокированной. Информацию о том, как обрабатывать открытые и неиспользуемые соединения, вы можете найти в документации к вашей базе данных.

Внимание

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

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

Смотрите также fbsql_pconnect(), ibase_pconnect(), ifx_pconnect(), ingres_pconnect(), msql_pconnect(), mssql_pconnect(), mysql_pconnect(), ociplogon(), odbc_pconnect(), oci_pconnect(), pfsockopen(), pg_pconnect() и sybase_pconnect().

Работа для фрилансеров на Freelancehunt — список всех доступных проектов для фрилансера в Украине

Нужен опытный дизайнер который имел дело с расширениями и нарисует качественный дизайн для нового расширения на хром, в ЛС обсудим все детали! Пожалуйста, покажите в заявках ваши похожие работы (Если есть)

Всем привет! нужен дизайн + вёрстка сайта + лого компании. бюджет: до 25000 грн за дизайн и верстку До 1400 грн за лого. Вот пример дизайна и вёрстки, который нас вдохновляет: https://www.thesoul-publishing.com/ Было бы очень хорошо…

Нужна помощь с GA для контентного проекта. Задача — оценить качество и верность текущей настройки сбора данных в GA, на основании которых предполагается наиболее точная оценка качества потребления контента (статьи), для последующей…

Розробити образ серії дитячих засобів побутової хімії та гігієни під існуючий логотоп. Описание продукта, для которого разрабатывается упаковка: засіб для миття посуду, рідке мило, засіб для прання Портрет целевой аудитории: жінки…

Наша команда створює додаток, який рахує кроки людини (педометр). Очевидно з деякими особливостями. Ми хочемо візуально показувати пройденний шлях людини, та мотивувати її дойти до наступного чекпоінта. За те, що людина пройде місячний…

Нужно сделать гибкий обмен товаров и заказов с сайтом: Сайт сделан на движке PHP фреймворк YII2 Нужно настоить обмен таким образом, что бы можно было выбирать, что я хочу обновить. 1)Обновить название; 2)Обновить картинку; 3)Обновить…

Ищу того кто пишет на Yii2. Дело в том, что кроме этой задачи есть еще 5 проектов на Yii2. Некоторые я хочу передать новому прогеру, так как старый, по личным обстоятельствам, не всегда может заниматься работой. .. ВНИМАНИЕ! Мы уже…

Доброго времени суток. Требуется дизайн одностраничного сайта компании, нужно что-то среднее между сайтом-визиткой и лендингом. Всего до 10 типовых блоков формата О компании, Как мы работаем, Свяжитесь с нами и пр. Полное ТЗ отправлю…

Приветствую. Цену указывайте за 1000 символов В поиске seo-копирайтера/рерайтера на долгосрочное сотрудничество, для написания информационно-продающих статей для сайта и социальных сетей. Нужно писать просто о сложном. Тема: Продажа…

Готовый сайт сделать как по шаблону (конструктивные изменения рассмотрим) Основной фон-картина (есть в наличии), текст на нем на бело-прозрачном фоне или без него (на Ваше усмотрение, посоветуйте, как лучше). Стиль шрифта пробуем как у…

Необходимо создать под ключ и наполнить сайт, не уступающий по уровню главным конкурентам (ссылка на их сайт ниже) https://ulf. ua/ru https://www.otpleasing.com.ua/ https://hapai.kiev.ua/ https://bestleasing.com.ua/ru/…

Всем привет. Работаем с Бинотел. Нужна CRM которая позволит вести email переписку с клиентами по определенному шаблону — письмо с запросом на заполнение гугл формы, потом дальнейшая переписка в несколько ступеней. Также СМС рассылка и…

Разыскиваю девелопера, с опытом работы с высоконагруженными системам(трафик/БД), для выполнения различных доработок, с соблюдением правил оптимизированной настройки. Желательные с такими подходами и качествами: если делать то на…

По готовым чертежам в Автокаде создать интересный современный проект Пивной ресторации . Летняя площадка+ ресторан (220м кв) . Ориентиры интерьера в приложении. Бюджет небольшой. Подойдёт для тех кто хочет в рекламных целях проявить. ..

Фирменный стиль магазина

Дизайн упаковки, Полиграфический дизайн

0

Arrayable, yii \ base \ Arrayable | Документация по API для Yii 2.0

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

Например, если класс реализует Arrayable, вызывая toArray (), экземпляр этого класса можно превратить в массив (включая все его встроенные объекты), который затем можно легко преобразовать в другие форматы, такие как JSON, XML.

Методы fields () и extraFields () позволяют реализующим классам настраивать, как и какие из их данных следует отформатировать и поместить в результат toArray ().

Подробности метода

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

Этот метод аналогичен fields (), за исключением того, что список объявленных полей этим методом не возвращаются по умолчанию toArray ().Только когда поле в списке явно запрашивается, будет ли он включен в результат toArray ().

См. Также:

публичный абстрактный массив extraFields ()
возврат массив

Список расширяемых имен или определений полей. Пожалуйста, обратитесь в fields () в формате возвращаемого значения.

Возвращает список полей, которые должны возвращаться по умолчанию функцией toArray (), если не указаны конкретные поля.

Поле — это именованный элемент в массиве, возвращаемом методом toArray ().

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

  функция ($ модель, $ поле) {
    
}
  

Например, следующий код объявляет четыре поля:

  • электронная почта : имя поля совпадает с именем свойства электронная почта ;
  • firstName и lastName : имена полей: firstName и lastName , а их значения берутся из свойств first_name и last_name ;
  • fullName : имя поля — fullName .Его значение получается путем объединения first_name и last_name .
  возврат [
    'электронное письмо',
    'firstName' => 'first_name',
    'lastName' => 'last_name',
    'fullName' => function ($ model) {
        вернуть $ model-> first_name.  ''. $ model-> last_name;
    },
];
  

См. Также toArray ().

публичный абстрактный массив поля ()
возврат массив

Список имен или определений полей.

Преобразует объект в массив.

публичный абстрактный массив toArray (массив $ fields = [], array $ expand = [], $ recursive = true)
$ fields массив

Поля, которые должен содержать выходной массив. Поля не указаны в полях () будут проигнорированы. Если этот параметр пуст, будут возвращены все поля, указанные в fields ().

$ развернуть массив

Дополнительные поля, которые должен содержать выходной массив. Поля, не указанные в extraFields (), игнорируются. Если этот параметр пуст, без дополнительных полей будет возвращен.

$ рекурсивный логическое

Следует ли рекурсивно возвращать представление массива встроенных объектов.

возврат массив

Представление объекта в виде массива

Beaten-Sect0r / yii2-db-manager: функции резервного копирования и восстановления базы данных

Нажмите ⭐!

Функции резервного копирования и восстановления баз данных MySQL / PostgreSQL

Установка

Предпочтительный способ установки этого расширения — через composer.

Либо запустить

Требуется композитор
 --prefer-dist beaten-sect0r / yii2-db-manager "*" 

или добавьте

  "битый-sect0r / yii2-db-manager": "*"
  

в требуемый раздел вашего файла composer. json .

Конфигурация

После установки расширения просто добавьте его в свою конфигурацию:

Базовая конфигурация / web.php

Расширенный backend / config / main.php

Простая конфигурация

 'modules' => [
        'db-manager' => [
            'class' => 'bs \ dbManager \ Module',
            // путь к каталогу для дампов
            'path' => '@ app / backups',
            // список зарегистрированных db-компонентов
            'dbList' => ['db'],
            // Адаптер Flysystem (необязательно) Creocoder \ flysystem \ LocalFilesystem будет использоваться по умолчанию.'flySystemDriver' => 'креокодер \ flysystem \ LocalFilesystem',
            'as access' => [
                'class' => 'yii \ filters \ AccessControl',
                'rules' => [
                    [
                        'allow' => true,
                        'role' => ['admin'],
                    ],
                ],
            ],
        ],
    ], 

Расширенная конфигурация

 'components' => [
        // https://github. com/creocoder/yii2-flysystem
        'backupStorage' => [
            'class' => 'creocoder \ flysystem \ FtpFilesystem',
            'host' => 'ftp.example.com ',
            // 'порт' => 21,
            // 'username' => 'your-username',
            // 'пароль' => 'ваш-пароль',
            // 'ssl' => true,
            // 'тайм-аут' => 60,
            // 'корень' => '/ путь / к / корню',
            // 'permPrivate' => 0700,
            // 'permPublic' => 0744,
            // 'пассивный' => ложь,
            // 'transferMode' => FTP_TEXT,
        ],
    ],
    'modules' => [
        'db-manager' => [
            'class' => 'bs \ dbManager \ Module',
            // Адаптер Flysystem (необязательно) Creocoder \ flysystem \ LocalFilesystem будет использоваться по умолчанию.'flySystemDriver' => 'креокодер \ flysystem \ LocalFilesystem',
            // путь к каталогу для дампов
            'path' => '@ app / backups',
            // список зарегистрированных db-компонентов
            'dbList' => ['db', 'db1', 'db2'],
            // время ожидания процесса
            'timeout' => 3600,
            // дополнительные пресеты mysqldump / pg_dump (доступны для выбора в формах дампа и восстановления)
            'customDumpOptions' => [
                'mysqlForce' => '--force',
                'somepreset' => '--triggers --single-transaction',
                'pgCompress' => '-Z2 -Fc',
            ],
            'customRestoreOptions' => [
                'mysqlForce' => '--force',
                'pgForce' => '-f -d',
            ],
            // опции для полной настройки генерации команд по умолчанию
            'mysqlManagerClass' => 'CustomClass',
            'postgresManagerClass' => 'CustomClass',
            // возможность добавления дополнительных DumpManager
            'createManagerCallback' => function ($ dbInfo) {
                if ($ dbInfo ['dbName'] == 'эксклюзивный') {
                    return new MyExclusiveManager;
                } еще {
                    вернуть ложь;
                }
            }
            'as access' => [
                'class' => 'yii \ filters \ AccessControl',
                'rules' => [
                    [
                        'allow' => true,
                        'role' => ['admin'],
                    ],
                ],
            ],
        ],
    ], 

Конфигурация консоли

 'modules' => [
        'db-manager' => [
            'class' => 'bs \ dbManager \ Module',
            // Адаптер Flysystem (необязательно) Creocoder \ flysystem \ LocalFilesystem будет использоваться по умолчанию. 'flySystemDriver' => 'креокодер \ flysystem \ LocalFilesystem',
            // путь к каталогу для дампов
            'path' => '@ app / backups',
            // список зарегистрированных db-компонентов
            'dbList' => ['db'],
        ],
    ], 

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

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

Pretty url’s / db-manager

Нет симпатичного URL index.php? R = db-manager

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

-db — компонент db, значение по умолчанию: db

-gz — архив gzip

— файловое хранилище

-f — имя файла, по умолчанию последний дамп

Создать дамп

 PHP дамп yii / создать -db = db -gz -s 

Восстановить дамп

 php yii дамп / восстановление -db = db -s -f = дамп.sql 

Удаление всех дампов

Проверить подключение к базе данных

 php yii дамп / тестовое соединение -db = db 

История изменений

  • Поддержка Flysystem
  • Поддержка консоли
  • Управление несколькими базами данных
  • Возможность настройки параметров дампа и восстановления; дамп и восстановление процессоров
  • Возможность асинхронного выполнения операций
  • Способность сжатия отвалов

dmstr / yii2-db: расширения базы данных для Yii 2.

0 Фреймворк

Около

dmstr \ db \ behavior \ HydratedAttributes

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

dmstr \ db \ mysql \ FileMigration

выполняет миграцию базы данных из файлов sql

Установка

Предпочтительный способ установки этого расширения — через composer.

Либо запустить

  композитору требуется --prefer-dist dmstr / yii2-db "*"
  

или добавьте

  "dmstr / yii2-db": "*"
  

в требуемый раздел вашего файла composer.json .

Конфигурация

dmstr \ console \ контроллеры

Включите его в конфигурацию консоли

  'controllerMap' => [
        'db' => [
            'class' => 'dmstr \ console \ controllers \ MysqlController',
            'noDataTables' => [
                'app_log',
                'app_session',
            ]
        ],
    ],
  

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

Команды

yii migrate. ..

Создание класса миграции файла

  yii перенести / создать \
    --templateFile='@vendor/dmstr/yii2-db/db/mysql/templates/file-migration.php 'init_dump
  
yii db ...
  ОПИСАНИЕ

Команда обслуживания базы данных MySQL для текущего (db) соединения


ПОДКОМАНДЫ

- db / create Создать схему
- db / destroy Удалить схему
- db / dump Схема дампа (все таблицы)
- db / export Экспорт таблиц (только INSERT)
- db / import Импорт из файла в базу данных и очистка кеша
- db / index (по умолчанию) Отображает таблицы в базе данных
- БД / ожидание соединения

Чтобы увидеть подробную информацию об отдельных подкомандах, введите:

  yii help <подкоманда>

  

Показать справку

  Справка по yii db
  

Примеры

Команда пробного пуска (доступна не для всех команд)

  yii db / создать корневой секрет -n
  

Уничтожить базу данных

  yii db / уничтожить корневой секрет
  

Дамп всех таблиц

  yii db / dump -o / dumps
  

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

  yii db / дамп -o / дампы \
  --db = dbReadonly \
  --noDataTables = app_audit_data, app_audit_entry, app_audit_error, app_audit_javascript, app_audit_mail
  

Дамп из вторичного соединения, импорт в первичный (по умолчанию)

  yii db / дамп -o / дампы \
    --db = dbReadonly \
    --noDataTables = app_audit_data, app_audit_entry, app_audit_error, app_audit_javascript, app_audit_mail \
 | xargs yii db / import --interactive = 0
  

Построен dmstr

Репликация базы данных Yii2 и разделение чтения-записи

Предисловие

Многие базы данных поддерживают репликацию базы данных для повышения доступности базы данных, сокращения времени отклика сервера и уменьшения нагрузки на базу данных. Благодаря функции репликации базы данных данные реплицируются с так называемого главного сервера на подчиненный сервер. Главный сервер выполняет добавления и удаления, а подчиненный сервер выполняет запросы.

Предварительное условие разделения чтения и записи: конфигурация синхронизации главного и подчиненного сервера базы данных Linux

Синхронизация данных двух серверов является предварительным условием разделения чтения и записи, но этого нет в учебнике Yii2 по разделению чтения и записи. Конфигурация разделения чтения и записи базы данных в yii2 реализует только чтение и запись в главной базе данных и запрос в подчиненной базе данных, поэтому сначала нам нужно настроить синхронизацию данных главного и подчиненного серверов.См. Дополнительные сведения в разделе «Конфигурация синхронизации главного-подчиненного устройства базы данных Linux»

Вложение: Синхронизация конфигурации прошла успешно, ошибка синхронизации вызвана неправильной работой или другими причинами. Как решить проблему? Просмотр: Решение ошибки синхронизации данных Mysql Master-Slave

Конфигурация разделения чтения и записи

После того, как синхронизация базы данных главного и подчиненного серверов Linux завершена, мы можем запустить конфигурацию разделения чтения и записи yii2.Официальные документы по этому поводу тоже есть, но изложение неясное, практических примеров нет. Я здесь, чтобы улучшить его.

1. Откройте файл конфигурации нашей базы данных commonconfigmain-local.php и настройте его в атрибуте DB следующим образом:

  'db' => [
    'class' => 'yii \ db \ Connection',
     
    // Настраиваем главный сервер
    'dsn' => 'mysql: host = 192.168.0.1; dbname = hyii2',
    'username' => 'root',
    'пароль' => 'корень',
    'charset' => 'utf8',
     
    // Настраиваем подчиненный сервер
    'slaveConfig' => [
        'username' => 'root',
        'пароль' => 'корень',
        'атрибуты' => [
            // использовать меньшее время ожидания подключения
            PDO :: ATTR_TIMEOUT => 10,
        ],
        'charset' => 'utf8',
    ],
     
    // Настраиваем подчиненный сервер 组
    'рабы' => [
            ['dsn' => 'mysql: host = 192. 168.0.2; dbname = hyii2 '],
        ],
],  

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

Приведенная выше конфигурация — только один главный-подчиненный. Если требуется один главный-подчиненный или несколько мастер-подчиненных, это можно сделать, обратившись к этому примеру и официальным документам.Официальные документы

PHP Yii2 — Миграция базы данных

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

Темы

Команды

Чтобы просмотреть полный список команд миграции yii, выполните следующую команду:

Вы получите следующий результат:

Создать миграцию

Как я сам объяснил, для создания миграции выполните следующую команду в терминале / командной строке:

  $ yii миграция / создание your_migration_name  

Например, для создания новой таблицы пользователей :

  $ yii миграция / создание create_users_table  

Затем будет запрошено подтверждение, введите да и нажмите Введите , после чего будет создан новый файл миграции. В папке проекта Yii вы найдете новую папку migrations и был создан файл m _create_users_table.php .

относится к дате и времени UTC, когда создается миграция.

Откройте файл миграции в вашем редакторе, вы найдете следующий код:

  используйте yii \ db \ Migration;


class m200725_073858_create_users_table расширяет миграцию
{
    
    публичная функция вверх ()
    {
        $ this-> createTable ('пользователи', [
            'id' => $ this-> primaryKey (),
        ]);
    }

    
    публичная функция вниз ()
    {
        $ this-> dropTable ('пользователи');
    }
}  

Как видите, в файле миграции есть две функции.

  • function up () — Функция up () — это коды, которые должны выполняться, когда мы запускаем миграцию в командной строке с помощью команды $ yii migrate .
  • function down () — Функция down () — это коды, которые будут выполняться, когда мы откатываем миграцию с помощью команды $ yii migrate / down .

Например, если мы создадим таблицу в функции up () , мы можем реализовать drop table в функции down () , или если мы добавим столбец в функцию up () , тогда мы можем реализовать drop column в down () функция

Теперь попробуйте запустить эту команду, затем да :

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

Шпаргалка

Весь приведенный ниже код протестирован в Yii версии 2.0.11

Создать таблицу

Создать таблицу и столбец
  $ this-> createTable ('table_name', [
'col_1' => $ this-> primaryKey (),
'col_2' => $ this-> строка (64),
]);  

будет соответствовать приведенному ниже SQL-запросу:

  СОЗДАТЬ ТАБЛИЦУ `имя_таблицы` (
`col_1` INT (11) NOT NULL AUTO_INCREMENT,
`col_2` VARCHAR (64) ПО УМОЛЧАНИЮ NULL,
ПЕРВИЧНЫЙ КЛЮЧ (`id`)
)  
С ДВИГАТЕЛЕМ и БЛОКОМ
  $ this-> createTable ('table_name', [
'col_1' => $ this-> primaryKey (),
], 'двигатель = InnoDb, charset = utf8');  
НЕПОДПИСАННОЕ, ЗНАЧЕНИЕ ПО УМОЛЧАНИЮ, НЕ НУЛЕЕ
  $ this-> createTable ('table_name', [
'col_2' => $ this-> bigPrimaryKey (64),



'col_3' => $ this-> integer (4) -> defaultValue (10) -> unsigned () -> notNull (),

]);  
Необработанный SQL
  $ this-> createTable ('table_name', [
'col_1' => 'INT (11) UNSIGNED NOT NULL AUTO_INCREMENT',
'col_2' => 'VARCHAR (10)',
'ПЕРВИЧНЫЙ КЛЮЧ (`col_1`)',
'KEY `col_2` ИСПОЛЬЗУЯ HASH (` col_2`)',
])  

Список типов данных

Этот тип данных сравнивается с типом данных MySQL

Числовой
Yii MySQL Замечания
целое число () ИНТ
tinyInteger () TINYINT Доступно с версии 2. 0,14
bigInteger () BIGINT
smallInteger () МАЛЕНЬКИЙ
десятичный () ДЕСЯТИЧНЫЙ
поплавок () ПОПЛАВКА
двойной () ДВОЙНОЙ
деньги () ДЕСЯТИЧНЫЙ (19, 4)

Логическое

Yii MySQL
логический () ТИНИИНТ (1)
Строка
Yii MySQL
симв. () СИМВОЛ
строка () VARCHAR
текст () ТЕКСТ
двоичный () BLOB
Дата и время
Yii MySQL
дата () ДАТА
время () ВРЕМЯ
datetime () ДАТА
отметка времени () ВРЕМЯ ВРЕМЕНИ
JSON
Yii MySQL Замечания
json () JSON Доступно с версии 2. 0,14

Откидной столик

  $ this-> dropTable ('имя_таблицы');  

Переименовать таблицу

  $ this-> renameTable ('old_table_name', 'new_table_name');  

Добавить столбец

  $ this-> addColumn ('имя_таблицы', 'имя_столбца', $ this-> integer ());  

Отводная колонна

  $ this-> dropColumn ('имя_таблицы', 'имя_столбца');  

Переименовать столбец

  $ this-> renameColumn ('table_name', 'old_column_name', 'new_column_name');  

Задняя колонна

  $ this-> alterColumn ('имя_таблицы', 'имя_столбца', $ this-> integer ());  

Добавить первичный ключ

  $ this-> addPrimaryKey ('primary_key_name', 'table_name', ['col_1', 'col_2']);  

Отбросить первичный ключ

  $ this-> dropPrimary ('имя_первого_ключа', 'имя_таблицы');  

Создать индекс

  $ this-> createIndex ('index_key_name', 'table_name', ['col_1', 'col_2']);  

Индекс падения

  $ this-> dropIndex ('index_key_name', 'table_name');  

Добавить внешний ключ

  $ this-> addForeignKey ('foreign_key_name', 'table_name', 'col_name', 'ref_table_name', 'ref_col_name');  

Отбросить внешний ключ

  $ this-> dropForeignKey ('foreign_key_name', 'table_name');  

Вставить записи

Вставить отдельную запись
  $ this-> insert ('table_name', [
'col_1' => 'значение 1',
'col_2' => 'значение 2',
'col_3' => 'значение 3',
]);  
Пакетная вставка
  $ this-> batchInsert ('table_name', ['col_1', 'col_2', 'col_3'], [
['record_1_value_1', 'record_1_value_2', 'record_1_value_3'],
['record_2_value_1', 'record_2_value_2', 'record_2_value_3'],
['record_3_value_1', 'record_3_value_2', 'record_3_value_3'],
]);  

Необработанный SQL

  $ this-> execute ('СОЗДАТЬ ТАБЛИЦУ. .. ');  

Изменить базу данных

В папке config создайте новое соединение с базой данных db2_name.php :

  возврат [
    'class' => 'yii \ db \ Connection',
    'dsn' => 'mysql: host = localhost; dbname = db2_name',
    'username' => 'root',
    'пароль' => '',
    'charset' => 'utf8',
];  

Затем в \ config \ web.php добавьте новый компонент базы данных:

\ config \ web.php

  $ params = требуется __DIR__.'/params.php';
$ db = требуется __DIR__. '/db.php';
$ db2 = требуется __DIR__. '/db2_name.php';
$ config = [
...,
'компоненты' => [
...,
'db' => $ db,
'db2' => $ db2,]
]  

В вашем файле миграции:

  используйте yii \ db \ Migration;


class m200725_073858_create_users_table расширяет миграцию
{
публичная функция init () {$ this-> db = 'db2'; родитель :: инициализация (); }
    
    публичная функция вверх ()
    {
        $ this-> createTable ('пользователи', [
            'id' => $ this-> primaryKey (),
        ]);
    }

    
    публичная функция вниз ()
    {
        $ this-> dropTable ('пользователи');
    }
}  

Кредиты

В основном я просто перевозчик документации Yii2 . Полную документацию можно найти здесь.

миграции баз данных — phd

: Bulb: рекомендуется разделять структурные (схемы) миграции и миграции только данных.

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

  • Создать файл-миграцию

      $ yii миграция / создание \
        [email protected]/db/mysql/templates/file-migration.php \
        --migrationPath = @ проект / миграции / dev-data \
        экспорт
      
  • Создайте экспорт базы данных, который представляет собой скорректированный дамп

      $ yii db / export --outputPath = @ проект / миграции / dev-data
      

    : восклицательный знак: дамп усекает все экспортированные таблицы по умолчанию

  • Создать миграцию

      $ yii file: миграция / создание dev_data
      
  • Настроить public $ file = '.sql '; во вновь созданной миграции.

  • Для разработки добавьте миграции данных разработки через переменную ENV в docker-compose.dev.yml

      APP_MIGRATION_LOOKUP = @ проект / миграции / dev-data
      

Дополнительная информация

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

  'params' => [
    'yii.migrations '=> [
        '@ app / migrations / data',
    ]
]
  

Пути поиска для миграций могут быть определены в конфигурации приложения, подробности см. В dmstr / yii2-migrate-command.

  'params' => [
    'yii.migrations' => [
        '@ yii / rbac / migrations',
        '@ dektrium / user / migrations',
        '@ vendor / lajax / yii2-translate-manager / migrations',
        '@ bedezign / yii2 / audit / migrations'
    ]
]
  

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

Предварительно настроенный псевдоним команды

Настроить команду миграции с предопределенными значениями использовать только для создания миграции файлов

  'controllerMap' => [
    'file: migrate' => [
        'class' => 'yii \ console \ controllers \ MigrateController',
        'templateFile' => '@ vendor / dmstr / yii2-db / db / mysql / templates / file-migration. php ',
        'migrationPath' => '@ project / migrations / dev-data',
    ]
],
  

Операторы MySQL ALTER TABLE

Следует объединить в один оператор, поскольку он быстрее и устойчивее к ошибкам.

  ALTER TABLE `my_table` ADD` COL_X` DECIMAL (10,2) NULL DEFAULT NULL ПОСЛЕ `COL_A`,
    ДОБАВИТЬ VARCHAR `COL_Y` (50) NULL ПО УМОЛЧАНИЮ NULL ПОСЛЕ` COL_B`,
    ДОБАВИТЬ VARCHAR `COL_Z` (5) NULL ПО УМОЛЧАНИЮ NULL ПОСЛЕ` COL_C`,
  

Ресурсы

: green_book: https: // github.com / dmstr / yii2-db / blob / master / README.md

Учебник

: разработка с Yii / PHP / MySQL с использованием Docker

Прошло около года с тех пор, как я перешел на разработку этого веб-сайта (созданного с помощью Yii 2 / PHP / MySQL) с использованием Docker. После всего лишь одной попытки я понял, что Docker — лучшее, что случилось с разработкой. Ну после Vim , конечно.

Итак, я решил написать краткое руководство по настройке среды разработки dockerised в надежде, что оно будет полезно для тех, кто никогда не пробовал Docker или испытывает трудности при запуске.

Предисловие

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

Многие веб-приложения используют базу данных для сохранения данных.В последние годы LAMP ( Linux , Apache , MySQL , PHP ) стал одним из наиболее часто используемых программных стеков. Этот веб-сайт также построен на этом наборе технологий.

В идеальном мире каждая среда разработки соответствует производственной, по крайней мере, с точки зрения версий программного обеспечения и функций (например, параметров компиляции и т. Д.). Все это, конечно, может быть установлено на ПК разработчика или на сервере разработки, но вы можете запустить в трудности с доступностью конкретных версий программного обеспечения, обновлениями компонентов и так далее. Например, Ubuntu недавно удалила PHP 5.x из своих репозиториев, что означает, что вам нужно скомпилировать его самостоятельно.

Контейнеры Docker

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

Контейнеры Docker предлагают отличную альтернативу всей этой суматохе. Docker позволяет запускать компоненты, такие как PHP , Apache HTTP Server и MySQL , внутри стандартных облегченных контейнеров со 100% воспроизводимыми конфигурациями и связывать эти контейнеры с помощью виртуальных сетей. Вам больше не нужно устанавливать множество пакетов, единственное, что нужно, — это сам движок Docker. И, наконец, что не менее важно, Docker бесплатен и имеет открытый исходный код.

Каждый контейнер описан в так называемом Dockerfile , простом текстовом файле, содержащем специальные команды.Контейнер всегда основан на образе ; Репозиторий Docker предоставляет огромное количество готовых образов для всех видов программного обеспечения. Поэтому каждый Dockerfile начинается с команды FROM , указывающей образ, на котором будет построен контейнер.

Конфигурация контейнера HTTP-сервера Apache

Итак, давайте отправимся в путь. Как я уже упоминал выше, этот веб-сайт построен на стеке LAMP и фреймворке Yii PHP. Для его запуска я использую следующие два отдельных контейнера:

  1. yktoo-app , который запускает Apache HTTP Server + PHP .
  2. yktoo-db , на котором работает MySQL .

Первый контейнер определяется с помощью следующего файла (назовем его Dockerfile-app , чтобы отличить его от другого контейнера):

  # Dockerfile-app

# Используйте PHP 5. 6 с Apache для базового образа
С php: 5.6-apache

# Включите мод Rewrite Apache
ЗАПУСТИТЬ cd / etc / apache2 / mods-enabled && \
    ln -s ../mods-available/rewrite.load

# Установить необходимые расширения PHP
# - GD
ЗАПУСТИТЬ apt-get update && \
    apt-get install -y libfreetype6-dev && \
    docker-php-ext-configure gd --with-freetype-dir = / usr / include / && \
    докер-php-ext-install -j $ (nproc) gd
# - mysql
ЗАПУСТИТЬ docker-php-ext-install -j $ (nproc) mysql pdo_mysql

# Копировать конфигурацию HTTP-сервера
КОПИРОВАТЬ 000-по умолчанию.conf / etc / apache2 / sites-available /
  

Приведенная выше команда COPY копирует файл конфигурации виртуального хоста ( 000-default.conf ) в контейнер; этот файл выглядит следующим образом:

  # 000-default.conf


    ServerName localhost
    Мастер ServerAdmin @ localhost

    DocumentRoot / var / www / html / web
    Информация о LogLevel php5: отладка

    ErrorLog $ {APACHE_LOG_DIR} /error. log
    CustomLog $ {APACHE_LOG_DIR} /access.log вместе

    <Каталог "/ var / www / html / web">
        # Запрещать .htaccess
        AllowOverride Нет
    

    # Настроить перезапись так, чтобы все запросы направлялись в index.php
    RewriteEngine на
    # если каталог или файл существует, использовать его напрямую
    RewriteCond / var / www / html / web% {REQUEST_FILENAME}! -F
    RewriteCond / var / www / html / web% {REQUEST_FILENAME}! -D
    # в противном случае пересылаем его в index.php
    RewriteRule. /var/www/html/web/index.php

  

Здесь важно использование AllowOverride None , поскольку оно запрещает Apache использовать стандарт .htaccess из исходного дерева. В этом файле у меня, например, есть перенаправление на HTTPS (и множество других перенаправлений), которое не нужно для разработки.

Конфигурация контейнера MySQL

Контейнер №2 выполняет другой жизненно важный компонент, сервер базы данных MySQL . Его файл конфигурации (назовем его Dockerfile-db ) имеет следующее содержимое:

  # Dockerfile-db

# Используйте MySQL 5.7 для базового образа
ИЗ mysql: 5.7.16

# Скопировать скрипты инициализации базы данных
COPY init.sql /docker-entrypoint-initdb.d/
КОПИРОВАТЬ database.sql / db /

  

В этом файле команды COPY переносят два сценария SQL в контейнер. Первый, init.sql , выполняет начальную загрузку базы данных:

  # init.sql

создать базу данных yktoo;
используйте yktoo;
источник /db/database.sql;

создать пользователя appuser, идентифицированного «appuserPasswd»;
предоставить все привилегии на yktoo. * appuser @ '%';

  

Он также вызывает второй сценарий, базу данных .sql , который представляет собой просто дамп производственной базы данных со всеми таблицами и их содержимым. Таким образом я получаю точную копию своего веб-приложения, работающего по адресу https://yktoo. com/.

Запуск контейнеров

Контейнеры должны запускаться таким образом, чтобы база данных стала доступной для HTTP-сервера. Первое, что нужно исправить, это конфигурация базы данных в Yii ( config / db.php ):

   'yii \ db \ Connection',
    'dsn' => 'mysql: host = yktoo-db; dbname = yktoo',
    'username' => 'appuser',
    'пароль' => 'appuserPasswd',
    'charset' => 'utf8',
    'tablePrefix' => 't_',
];
?>
  

Как видите, он использует то же имя БД ( yktoo ), имя пользователя ( appuser ) и пароль пользователя ( appuserPasswd ), что и в ранее перечисленном init.sql . Также важно, чтобы имя хоста yktoo-db совпадало с именем контейнера MySQL, что позволяет получить к нему доступ из контейнера HTTP-сервера Apache после их связывания (см. Ниже).

Перед запуском контейнеров их образы должны быть созданы с использованием ранее созданных Dockerfile-app и Dockerfile-db :

  # build. sh

# Путь к корню вашего проекта
PROJ_ROOT = / путь / к / вашему / yii / app

# Создаем образ контейнера приложения
docker build -t yktoo-app-image -f "Dockerfile-app" "$ PROJ_ROOT"

# Создаем образ контейнера БД
docker build -t yktoo-db-image -f "Dockerfile-db" "$ PROJ_ROOT"

  

Это приведет к созданию двух образов Docker: yktoo-app-image и yktoo-db-image .Теперь можно запускать контейнеры:

  # run.sh

# Путь к корню вашего проекта
PROJ_ROOT = / путь / к / вашему / yii / app

# Сначала запускаем контейнер БД
docker run -d -e MYSQL_ROOT_PASSWORD = root --name yktoo-db yktoo-db-image

# Затем контейнер приложения и привязка его к базе данных
докер запустить -d \
    -p 80:80 \
    -v "$ PROJ_ROOT": / var / www / html \
    --name yktoo-app \
    --ссылка yktoo-db \
    yktoo-app-image

  

Параметр -p 80:80 связывает порт 80 внутри контейнера с портом 80 на хосте, что делает приложение доступным по адресу http: // localhost / .

Очень важно, чтобы наш контейнер БД назывался yktoo-db , потому что это то же имя, которое упоминается в config / db.php выше; кроме того, он используется для связывания контейнера HTTP-сервера с БД (параметр --link ).

Еще одним важным аспектом является использование опции -v , которая монтирует исходное дерево вашего проекта прямо в каталог / var / www / html на сервере Apache. Он позволяет сразу видеть изменения, внесенные в код приложения, без перезапуска контейнера, что очень удобно.

Остановка контейнеров

Я использую следующий скрипт для остановки запущенных контейнеров:

  # stop.sh

set -e

echo "Остановка контейнеров ..."
докер остановить yktoo-db yktoo-app

echo "Удаление контейнеров ..."
Докер RM yktoo-db yktoo-app

эхо "Готово".
  

Он останавливает и сразу же удаляет оба контейнера, так что при следующем запуске run. sh вы получите новую, 100% идентичную копию всей среды, включая базу данных.

По этой причине, если при разработке требуется изменение базы данных, эти изменения должны быть сохранены в отдельном файле (например, upgrade.sql ), затем вы заставляете его появляться внутри контейнера БД (добавляя COPY upgrade.sql / db / до Dockerfile-db ) и вызывая в init.sql (добавляя source / db / upgrade. sql; после строка source /db/database.sql; ).

Бонус: docker-compose

Управлять контейнерами с помощью приведенных выше скриптов довольно просто, но почему бы не пойти еще дальше? Docker предлагает специальный инструмент под названием docker-compose для управления конфигурациями нескольких контейнеров.Он позволяет описывать все связанные службы в одном файле, обычно называемом docker-compose.yml , и управлять ими с помощью еще более простых команд.

В Linux docker-compose не устанавливается как часть Docker Engine, поэтому его нужно устанавливать отдельно.

Ваш docker-compose.yml может выглядеть следующим образом:

  версия: "3"
Сервисы:
    приложение:
        строить:
            контекст:.
            dockerfile: ./Dockerfile-app
        имя_контейнера: yktoo-app
        порты:
            - «80:80»
        объемы:
            -.: / var / www / html
    db:
        строить:
            контекст:.
            dockerfile: ./Dockerfile-db
        имя_контейнера: yktoo-db
        среда:
            MYSQL_ROOT_PASSWORD: "корень"
  

Пути для контекста и томов должны указывать на (относительный) путь к корню исходного дерева. Когда у вас есть этот файл, контейнеры можно просто запустить с помощью:

. Это также позволит вам увидеть вывод обоих контейнеров в вашей консоли. Чтобы остановить их, используйте Ctrl + C или, как вариант, команду docker-compose stop с другой консоли; и удалить их docker-compose rm .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *