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’);
Здесь в папке default содержится ваше приложение, разбитое на подпапки Layout, Modules, Skin. Layout содержит шаблоны со структурой страниц, Modules содержит контроллеры, шаблоны и классы модулей, а Skin - шаблоны общих визуальных элементов.
Почему все хранится в папке default? Pina поддерживает перегрузку шаблонов для темы оформления сайта. И темы хранятся в папках на одном уровне с default. Таким образом у вас всегда в default оригинальные шаблоны, а папке шаблона - перегруженные.
Содержит несколько стандартных файлов для запуска фреймворка
Содержит настроечный файлы
Здесь храним файл index.php, который обрабатывает все HTTP запросы, так же здесь храним статику (css, js, картинки).
Папка для юнит-тестов.
Папка, где хранятся скомпилированные версии шаблонов, временные файлы, логи.
...
Приложение разбивается на модули. Каждый модуль - это определенное пространство имен (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 для хранения файлов локализации приложения.
Чтобы повысить эффективность роутинга и избавиться от традиционных таблиц роутинга, связывающих маски адресов с контроллерами, в 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-страницы, подключая и отключая их прямо из шаблонизатора.
У контроллера может быть несколько разных типов представлений. В данный момент на уровне автоматического определения поддерживаются два основных: 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.
В папке 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-функции модуля можно положить в папку 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 подключается к базе данных и выполняет базовые запросы. Методы класса на вход обычно получают уже готовый запрос, и управляют в основном форматом результата.
Получить несколько записей из таблицы с помощью метода 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");
Получить колонку:
$db = App::container()->get(\Pina\DatabaseDriverInterface::class);
$ids = $db->column("SELECT id FROM products WHERE title LIKE 'test%'");
Получить значение определенной ячейки
$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 проверяет соответствует ли поле значению или одному из значений массива.
SQL::table('cody_user')->orderBy('user_created desc')->groupBy('user_id')->calculate('count(cody_account.*) as cnt')->having('cnt > 5')->get();
Методы orderBy, groupBy, having просто добавляют соответствующие выражения “как есть”.
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’ => ‘[email protected]’]);
SQL::table('cody_user')->insertGetId(['user_login' => 'test', 'user_email' => '[email protected]']);
SQL::table('cody_user')->insert([['user_login' => 'test', 'user_email' => '[email protected]'], ['user_login' => 'test2', 'user_email' => '[email protected]']]);
SQL::table('cody_user')->put(['user_login' => 'test', 'user_email' => '[email protected]']);
SQL::table('cody_user')->putGetId(['user_login' => 'test', 'user_email' => '[email protected]']);
SQL::table('cody_user')->whereBy('user_login', 'test')->update(['user_email' => '[email protected]']);
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' => '[email protected]']);
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'],
];