pinahq/framework

The Pina2 Framework.


README

Введение

Pina Framework - PHP фреймворк для разработки RESTfull, stateless приложений, со встроенной поддержкой очередей (gearman) и асинхронной обработкой событий. На уровне работы с БД Pina поддерживает автоматическую генерацию структуры таблиц и триггеров на основании классов модели (как альтернативу миграциям).

Наша стратегия

Придерживаться стандартов PSR, схожего с Laravel именования методов и классов там, где это принципиально не влияет на производительность и идеологию фреймворка.

Наша идеология

Способствовать разработке производительных асинхронных RESTfull и stateless приложений на PHP.

Основывается на нескольких принципиальных позициях:

  • Структура БД и триггеры - должны храниться в кодовой базе проекта вместе с другой бизнес логикой и генерироваться автоматически.
  • Основа роутинга - однозначное отображение RESTfull ресурсов “коллекция/элемент/коллекция…” на контроллеры, позволяющий отказаться от больших таблиц роутинга.
  • Модульность с привязкой к пространствам имен (namespace).
  • Широкое применение очередей и фоновых процессов для затратных операций (пока на базе gearman).

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

Установка

Pina не гарантирует в будущем поддержку версий PHP младше PHP7, но в данный момент корректно работает на PHP 5.6+.

Требует PHP-расширений:

  • mysqli
  • mbstring
  • xml

Для начала работы нужно скачать bootstrap версию приложения (скоро будет на github и в composer), обновить зависимости через composer, прописать корректные настройки адреса сайта и БД, и запустить php pinacli.php system.update для обновления БД.

Настройки

В папке config расположены конфигурационные файлы. Каждый из них представляет собой PHP-файл, возвращающий ассоциативный массив. Для обращения к настройкам используется класс \Pina\Config.

Получить все настройки из файла config/app.php:

 \Pina\Config::load(‘app’);

Получить пользователя для подключения к БД,

\Pina\Config::get(‘db’, ‘user’);

Структура папок:

Папка app

Здесь в папке default содержится ваше приложение, разбитое на подпапки Layout, Modules, Skin. Layout содержит шаблоны со структурой страниц, Modules содержит контроллеры, шаблоны и классы модулей, а Skin - шаблоны общих визуальных элементов.

Почему все хранится в папке default? Pina поддерживает перегрузку шаблонов для темы оформления сайта. И темы хранятся в папках на одном уровне с default. Таким образом у вас всегда в default оригинальные шаблоны, а папке шаблона - перегруженные.

Папка bootstrap

Содержит несколько стандартных файлов для запуска фреймворка

Папка config

Содержит настроечный файлы

Папка public

Здесь храним файл index.php, который обрабатывает все HTTP запросы, так же здесь храним статику (css, js, картинки).

Папка tests

Папка для юнит-тестов.

Папка var

Папка, где хранятся скомпилированные версии шаблонов, временные файлы, логи.

Обработка ошибок и логирование

...

Модульность

Приложение разбивается на модули. Каждый модуль - это определенное пространство имен (namespace) внутри Pina\Modules. Например Pina\Modules\Catalog.

Обычно код модуля расположен в папке внутри app/default/Modules (так как автозагрузка по стандарту PSR-4 настроена именно таким образом, что не мешает вам подгрузить новый модуль через composer).

Ключевым элементом модуля является класс Module внутри пространства имен модуля. Именно его система подгружает при инициализации модуля. И именно он указывает на точное местоположение модуля.

Класс модуля реализует интерфейс Pina\ModuleInterface. Поэтому он должен определить как минимум четыре метода:

  • getPath() - возвращает путь к каталогу модуля
  • getNamespace() - возвращает название пространства имен модуля
  • getTitle() - возвращает имя модуля
  • boot() - запускается каждый раз при старте приложения.

Так же класс модуля может реализовать методы под разные режимы запуска приложения. Например, метод http() будет вызываться каждый раз, когда запускается обработка http-запроса. А метод cli() каждый раз, когда запускается обработка команды из командной строки. Эти методы должны вернуть настройки роутинга для своего типа приложения.

Так же обычно в классе модуля дополнительно объявляется функции в пространстве имен модуля. Например, функция локализации __()

Пример, объявления класса модуля:

<?php

namespace Pina\Modules\Images;

use Pina\ModuleInterface;
use Pina\Event;

class Module implements ModuleInterface
{

    public function getPath()
    {
        return __DIR__;
    }
    
    public function getNamespace()
    {
        return __NAMESPACE__;
    }

    public function getTitle()
    {
        return 'Images';
    }
    
    public function boot()
    {
        Event::subscribe($this, 'image.resize');
    }

    public function http()
    {
        return [
            'cp/images',
            'images',
        ];
    }
    
    public function cli()
    {
        return [
            'image',
        ];
    }

}

function __($string)
{
    return \Pina\Language::translate($string, __NAMESPACE__);
}

Здесь в методе boot() региструруется обработчик для события 'image.resize', в методе http() регистрируются обработчики на коллекции images и cp/images, а в методе cli() регистрируется пространство команд image.

Кроме классов, относящихся к пространству имен модуля, в модуле предусмотрено несколько папок:

  • Папка http с контроллерами и представлениями для обработки HTTP-запросов.
  • Папка helpers для собственных smarty-функций модуля.
  • Папка events для обработчиков событий
  • Папка cli для контроллеров комманд командной строки.
  • Папка lang для хранения файлов локализации приложения.

Слой HTTP

Роутинг

Чтобы повысить эффективность роутинга и избавиться от традиционных таблиц роутинга, связывающих маски адресов с контроллерами, в Pina существует ряд ограничений на структуру системных URL-адресов. Впрочем, если вас не устроит эта система, то фреймворк дает вам возможность зарегистрировать свой диспетчер и обрабатывать произвольные адреса самостоятельно, что очень полезно для CMS систем. Но об этом в другом разделе.

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

Pina требует от вас организовывать структуру URL в соответствии с REST-методологией. То есть структурировать URL-адреса по принципу “Коллекция/Элемент”. Таким образом список книг имел бы адрес /books, а книга с ID=5 имела бы адрес /books/5. Книги пользователя alex имели бы адрес /users/alex/books. Система интуитивно понятна, но имеет важное ограничение: Вы не можете задать коллекцию внутри коллекции минуя элемент. То есть адрес /users/books будет трактоваться как пользователь books внутри коллекции users.

Это ограничение дает возможность однозначно отображать множество коллекций на контроллеры, отбросив части с элементами. Например, GET-запрос к ресурсу /users/alex/books будет обрабатываться группой контроллеров в папке проекта /users/books

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

HTTP протокол поддерживает разные методы запросов, но обычно используются GET, POST, PUT, DELETE, так как их очень удобно сопоставлять с типовыми CRUD-операциями,

HTTP-метод CRUD-операция
POST CREATE (создание)
GET READ (чтение)
PUT UPDATE (обновление)
DELETE DELETE (удаление)

Если трактовать HTTP-методы, как CRUD операции, то сопоставив их с ресурсами в терминах “Коллекция/Элемент” получим следующие трактовки

HTTP-метод Объект Пример Трактовка
GET Коллекция GET /users Получить список пользователей
GET Элемент GET /users/alex Получить данные пользователя alex
POST Коллекция POST /users Добавить пользователя в коллекцию
POST Элемент POST /users/alex Добавить пользователя в коллекцию с определенным идентификатором
PUT Коллекция PUT /users Обновить информацию о пользователях
PUT Элемент PUT /users/alex Обновить данные о пользователе alex
DELETE Коллекция DELETE /users Удалить пользователей
DELETE Элемент DELETE /users/alex Удалить пользователя alex

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

Пример запроса Обработчик
GET /users /users/index.php
GET /users/alex /users/show.php
GET /users/create /users/create.php
POST /users /users/store.php
POST /users/alex /users/store.php
PUT /users /users/update.php
PUT /users/alex /users/update.php
DELETE /users /users/destroy.php
DELETE /users/alex /users/destroy.php

Обратите внимание, что в целом контроллер определяется HTTP-методом.

Но для GET-запросов есть исключение (особый метод create). Таким образом, для GET-запросов система отличает:

  • коллекцию (index.php),
  • элемент (show.php),
  • и ресурс для ввода информации для создания нового элемента (create.php).

Как это будет работать для вложенных коллекций:

Пример запроса Обработчик
GET /users/alex/books /users/books/index.php
GET /users/alex/books/5 /users/books/show.php
GET /users/alex/books/create /users/books/create.php
POST /users/alex/books /users/books/store.php
POST /users/alex/books/5 /users/books/store.php
PUT /users/alex/books /users/books/update.php
PUT /users/alex/books/5 /users/books/update.php
DELETE /users/alex/books /users/books/destroy.php
DELETE /users/alex/books/5 /users/books/destroy.php

Контроллеры хранятся в папке http в корне модуля. Так как модулей у нас может много, то надо определять, какой модуль отвечает за какую коллекцию. Для этого модулю достаточно подтвердить факт владения коллекцией в методе http класса модуля:

class Module implements ModuleInterface
{
...
    public function http()
    {
    	//здесь список коллекций, за которые отвечает модуль
        return [
            'users',
        ];
    }

Метод http модуля запускается каждый раз при старте приложения в начале обработки HTTP-запроса, а в качестве результата работы передает список коллекций (контроллеров), которые он обрабатывает.

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

class Module implements ModuleInterface
{
...
    public function http()
    {
        return [
            'users/books',
        ];
    }

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

Контроллеры

Итак, мы разобрались, где должен лежать файл контроллера, чтобы обрабатывать определенные URL-адреса. Теперь поговорим о том, как работает такой контроллер.

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

Для реализации этих целей контроллер работает с классом \Pina\Request и Pina\Response.

Получить параметры можно так: Request::input(‘id’);

Результаты контроллер возвращает через конструкцию return:

return [
    'resources' => $resources,
    'paging' => $paging->fetch(),
];

В таком случае контроллер передаст дальше данные, которые будут инерпретироваться в зависимости от выбранного представления. Но контроллер может так-же вернуть данные в каком-то определенном формате. Например, в JSON:

return Response::ok()->json($resource);

Предположим, наш контроллер на основе параметра ‘post_id’ должен получить из базы данных запись в блоге и вернуть её в качестве результата.

$postId = Request::input(‘post_id’);
$post = PostGateway::instance()->find($postId);
return $post;

Усложним задачу. Теперь если запись не найдена, надо возвращать страницу 404:

$postId = Request::input(‘post_id’);
$post = PostGateway::instance()->find($postId);
if (empty($post)) {
	return Response::notFound();
}
return $post;

Если не передан обязательный параметр post_id, возвращать код ошибки 400 bad request:

$postId = Request::input(‘post_id’);
if (empty($postId)) {
	return Response::badRequest();
}
$post = PostGateway::instance()->find($postId);
if (empty($post)) {
	return Response::notFound();
}
return $post;

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

Для доступа к параметрам запроса в классе \Pina\Request есть набор методов:

  • Получить все параметры в виде массива: Request::all();
  • Получить конкретный параметр: Request::input('name');
  • Получить конкретный параметр и в случае его отсутствия использовать значение по умолчанию: Request::input('name', 'default');
  • Получить некоторые параметры: Request::only('key1', 'key2', 'key3'); или Request::only(['key1', 'key2', 'key3']);
  • Получить все параметры кроме некоторых: Request::except('key1', 'key2', 'key3'); или Request::except(['key1', 'key2', 'key3']);
  • Получить некоторые параметры, если они присутсвиуют: Request::intersect('key1', 'key2', 'key3'); или Request::intersect(['key1', 'key2', 'key3']);

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

Валидация

Вложенные запросы.

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

Представление (View)

У контроллера может быть несколько разных типов представлений. В данный момент на уровне автоматического определения поддерживаются два основных: JSON-представление и HTML-представление. Но и вы можете возвращать из контроллера любое представление, используя конструкцию return с конкретным экземпляром интерфейса \Pina\ResponseInterface.

Чтобы получить JSON-представление достаточно обратиться к ресурсу, используя заголовок Accept: application/json или Accept: text/json. В этом случае данные, переданные как результаты запроса, упакуются в json-объект. В целом проектировать контроллер нужно таким образом, чтобы результаты его работы коррелировали с сутью запроса к нему. Думать о его результатах отдельно от его представления, даже если основным представлением будет HTML, а не JSON.

Для построения HTML-представления используется шаблонизатор Smarty. Шаблон лежит в той же папке, что и контроллер, называется также (за исключением расширения: tpl, а не php). У одного контроллера может быть несколько HTML-представлений. Выбор представления управляется параметром display.

Например:

Метод Ресурс Контроллер Шаблон
GET books/5 books/show.php books/show.tpl
GET books/5?display=edit books/show.php books/show.edit.tpl
GET books/create books/create.php books/create.tpl
GET books/create?display=copy books/create.php books/create.copy.tpl
POST books books/store.php books/store.tpl
PUT books/5 books/update.php books/update.tpl
DELETE books/5 books/delete.php books/delete.tpl

Обычно POST, PUT и DELETE запросы реализуют без HTML-представлений, просто перенаправляя клиента на нужный адрес. В этом случае контроллер должен вернуть экземпляр класса Response с указанием адреса перехода:

return Response::found(App::link('resources/:resource_id', ['resource_id' => $resourceId]));

Шаблоны

Smarty - мощный шаблонизатор, с основными его функциями вы можете познакомиться на официальном сайте Smarty, я же изложу основные приемы и smarty-функции, которые мы используем для строительства приложения на основе Pina.

Родительский шаблон (Layout)

В папке app/default/Layout хранятся родительские шаблоны, в один которых будут вписаны результаты отрисовки запроса. По умолчанию используется шаблон main.tpl, но вы можете в конкретном отображении поменять родительский шаблон через smarty-функцию:

{extends layout=”single”}

В таком случае будет использован родительский шаблон single.tpl

В родительский шаблон результат отрисовки будет передан в качестве переменной {$content}.

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

Родительский шаблон мог бы выглядеть так:

main.tpl

<html>
<head>
  <title>{place name=”title”}</title>
</head>
<body>
<h1>{place name=”title”}</h1>
<article>{$content}</article>
</body>
</html>

Чтобы отдельно прокинуть контент в область для title, в дочернем шаблоне надо использовать smarty-функцию {content}

show.tpl

{content name=title}{$post.post_title}{/content}

<p>{$post.post_text}</p>

Использование других контроллеров в отображении

Из представления вы можете обращаться к другим контроллерам через smarty-функцию {module}.

Например:

{module get="books/:book_id" book_id=$book.book_id}

В этом случае выполнится обработчик GET-запроса "books/:book_id", где заместо :book_id подставится значение из переменной $book.book_id. Результат его отрисовки в HTML и подставится в место вызова.

В эту smarty-функцию можно добавить дополнительные параметры, в том числе параметр display для выбора конкретного отображения.

Хорошей стратегией будет делать простые контроллеры и по необходимости компоновать их между собой в шаблонах.

Pina позволяет вам использовать отображение другого контроллера без вызова самого контроллера, если у вас уже есть подготовленные данные. Для этого можно использовать smarty-функцию {view}

{view get="books/:book_id" book=$book}

В этом случае переменная book будет проброшена в шаблон, как если бы она была вычислена контроллером.

Ссылки

Для построения ссылок между страницами используется smarty-функция {link}

{link get="books/:book_id" book_id=$book.book_id}

Этот пример сформирует ссылку на ресурс books/:book_id, подставив заместо :book_id значение $book.book_id. Параметры, которые нельзя подставить в шаблон ссылки, добавляются обычными GET-параметрами.

{link get="books/:book_id" book_id=5 display=edit}

Превратится в ссылку /books/5?display=edit

Контекст ссылок

В шаблон ссылки подставляются значения из параметров ссылки. А что если какого-то элемента для подстановки в параметре нет? В этом случае подставится значение из контекста. Контекст может быть глобальным и локальным.

Глобальный контекст настраивается с помощью инструкции (обычно вызываемой в методе http() класса модуля):

Route::context('language', 'ru');

Теперь во всех ссылках {link get="panel/:language/books"}, часть :language будет заменена на 'ru', если в параметрах не указано другое значение. Глобальный контекст действует только на замены.

Для настройки локального контектса существует блоковая смарти функция {link_context}, она добавляет ко всем вложенным в неё ссылкам свои параметры, даже если там нет таких замен.

{link_context author_id=$filter.author_id sort=$sort}
  <ul class="nav">
    {section loop=$pages name=page}
      <li><a href="{link get="books" page=$page}"></a></li>
    {/section}
  </ul>
{/link}

Каждая ссылка внутри вызова {link_context} получит параметр author_id и sort.

Свои smarty-функции

Собственные smarty-функции модуля можно положить в папку helpers  модуля.

Локализация

В php используется функция

__("Оригинальная строка на перевод")

В шаблонах используется блоковая smarty-функция {t} или модификатор |t

{t}Оригинальная строка на перевод{/t}

{some_function v="Оригинальная строка перевод"|t}

База данных

Настройка подключения к БД

За настройки реквизитов подключения к БД отвечает файл config/db.php. Например:

return array(
    'default' => array(
        'host' => 'localhost',
        'port' => '3306',
        'user' => 'root',
        'pass' => 'root',
        'base' => 'pina2',
        'charset' => 'utf8',
    )
);

Класс DB

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

Получить несколько записей из таблицы с помощью метода table:

$db = App::container()->get(\Pina\DatabaseDriverInterface::class);
$data =$db->table("SELECT * FROM products");

Получить одну строку с помощью метода row:

$db = App::container()->get(\Pina\DatabaseDriverInterface::class);
$item = $db->row("SELECT * FROM products");

column

Получить колонку:

$db = App::container()->get(\Pina\DatabaseDriverInterface::class);
$ids = $db->column("SELECT id FROM products WHERE title LIKE 'test%'");

value

Получить значение определенной ячейки

$db = App::container()->get(\Pina\DatabaseDriverInterface::class);
$id = $db->value("SELECT id FROM products WHERE title LIKE 'test%'");

Выполнить запрос, который не возвращает результата:

$db = App::container()->get(\Pina\DatabaseDriverInterface::class);
$db->query("DELETE FROM products WHERE id=5");

Получить ID записи после её вставки в таблицу

$db = App::container()->get(\Pina\DatabaseDriverInterface::class);
$db->query("INSERT INTO products SET title='test'");
$id = $db->insertId();

Конструктор запросов.

Получить все записи из таблицы:

$users = SQL::table('cody_user')->get();

Получить одну запись из таблицы (первую в выборке)

$user = SQL::table('cody_user')->whereBy("user_login", "admin")->first();

Получить значение конкретной ячейки

$isEnabled = SQL::table('cody_user')->whereBy("user_login", "admin")->value(‘user_enabled’);

Получить колонку из таблицы

$ids = SQL::table('cody_user')->column(‘user_id’);

Выборка с определенным полями

$users = SQL::table('cody_user')->select('user_login')->select('user_email')->get();

Метод select() в отличии от laravel добавляет указанные поля в выборку, а не заменяет их, то есть можно использовать несколько select с разными полями

Выборка по произвольному условию

SQL::table('cody_user')->where("user_expired > '2015-03-01'")->whereBy('user_enabled', 'Y')->get();

whereBy проверяет соответствует ли поле значению или одному из значений массива.

Order by, Group by, having

SQL::table('cody_user')->orderBy('user_created desc')->groupBy('user_id')->calculate('count(cody_account.*) as cnt')->having('cnt > 5')->get();

Методы orderBy, groupBy, having просто добавляют соответствующие выражения “как есть”.

Limit

SQL::table('cody_user')->limit(30, 10)->get();
SQL::table('cody_user')->limit(10)->get();

Соединения таблиц

SQL::table('cody_user')->innerJoin(SQL::table('cody_account')->on('user_id'))->whereBy('user_id', 5)->get();
SQL::table('cody_user')->leftJoin(
    SQL::table('cody_account')->on('user_id')->onBy('account_enabled', 'Y')
)->whereBy('user_id', 5)->get();

Агрегатные функции

SQL::table('cody_user')->whereBy('user_enabled')->count();
SQL::table('cody_user')->max('user_id');
SQL::table('cody_user')->min('user_id');
SQL::table('cody_user')->avg('user_id');
SQL::table('cody_user')->sum('user_id');

Проверка существования

if (SQL::table('cody_user')->whereBy('user_login', 'test')->exists()) {
    echo 'exists!';
}

Вставка данных

SQL::table('cody_user')->insert([‘user_login’ => ‘test’, ‘user_email’ => ‘test@test.com’]);

Получение идентификатора после вставки

SQL::table('cody_user')->insertGetId(['user_login' => 'test', 'user_email' => 'test@test.com']);

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

SQL::table('cody_user')->insert([['user_login' => 'test', 'user_email' => 'test@test.com'], ['user_login' => 'test2', 'user_email' => 'test2@test.com']]);

Замена данных в таблице

SQL::table('cody_user')->put(['user_login' => 'test', 'user_email' => 'test@test.com']);

Замена данных в таблице и получения идентификатора, если добавлен новый элемент

SQL::table('cody_user')->putGetId(['user_login' => 'test', 'user_email' => 'test@test.com']);

Обновление данных

SQL::table('cody_user')->whereBy('user_login', 'test')->update(['user_email' => 'test@test.com']);

Удаление данных

SQL::table('cody_user')->whereBy('user_login', 'test')->delete();

Модели и шлюзы таблиц

Модели, которые непосредственно связаны с одной таблицей в БД, обычно наследуются от класса TableDataGateway. Этот базовый класс дает модели инструментарий для описания и обработки мета-информации таблицы, а так же для гибкой выборки данных из неё. Так как TableDataGateway сам унаследован от класса контруктора запросов SQL, то модель получает все методы конструктора запросов, проинициализированного таблицей моделей. Что позволяет использовать всю мощь конструктора запросов, скрывая обращение к конкретным таблицам обращением к классам моделей. Пример для соединения таблиц на таких моделях можно было бы переписать следующим образом:

UserGateway::instance()->leftJoin(
    AccountGateway::instance()->on('user_id')->enabled()
)->whereId(5)->get();

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

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

UserGateway::instance()->find(5);

Получить все данные из таблицы

UserGateway::instance()->get();

Получить идентификатор (первичный ключ) записи

UserGateway::instance()->whereBy('user_login', 'test')->id();

Выборка по идентификатору (первичному ключу)

UserGateway::instance()->whereId(5)->first();

Получить активные записи (у которых поле enabled = ‘Y’)

UserGateway::instance()->enabled()->get();

Удалить пользователя по его идентификатору

UserGateway::instance()->whereId(5)->delete();

Обновить данные пользователя по его идентификатору

UserGateway::instance()->whereId(5)->update(['user_email' => 'test@test.com']);

Генерация структуры таблицы

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

class UserGateway extends TableDataGateway
{
    protected static $table = "user";
    protected static $fields = array(
        'id' => "int(10) NOT NULL AUTO_INCREMENT",
        'image_id' => "int(10) NOT NULL DEFAULT '0'",
        'login' => "varchar(32) NOT NULL DEFAULT ''",
        'password' => "varchar(64) NOT NULL DEFAULT ''",
        'email' => "varchar(64) NOT NULL DEFAULT ''",
        'firstname' => "varchar(64) NOT NULL DEFAULT ''",
        'lastname' => "varchar(64) NOT NULL DEFAULT ''",
        'middlename' => "varchar(64) NOT NULL DEFAULT ''",
        'phone' => "varchar(64) NOT NULL DEFAULT ''",
        'status' => "enum('new','active','suspensed','disabled') NOT NULL DEFAULT 'new'",
        'access_group' => "enum('registered','admin') NOT NULL DEFAULT 'registered'",
        'created' => "timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP",
    );
    protected static $indexes = array(
        'PRIMARY KEY' => 'id',
        'UNIQUE KEY login' => 'login',
    );
}

В поле static::$table указываем имя таблицы в БД.

В массиве static::$fields указываем список полей, ключи массива - названия полей, а значения - описание поля в терминах MySQL.

В массиве static::$indexes указываем индексы таблицы. В качестве ключа массива используем тип ключа и имя ключа, а в качестве значений - набор полей. Если полей несколько, то надо использовать массивы.

Например:

    protected static $indexes = [
        'PRIMARY KEY' => 'id',
        'KEY name' => ['firstname', 'lastname'],
    ];