- PVSM.RU - https://www.pvsm.ru -
За прошлый год в PHPixie добавилось много новых возможностей и несколько компонентов, к тому же немного изменилась стандартная структура бандла чтобы снизить порог вхождения для разработчиков. Так что пришло время создать новый туториал, и в этот раз мы попробуем сделать его чуть по другому. Вместо того чтобы просто смотреть на готовый демо проект с описанием, мы будем идти постепенно, при чем на каждой итерации у нас будет полностью рабочий сайт. Мы будем строить простенький цитатник с логином, регистрацией, интеграцией с соцсетями и консольными командами для статистики. Полная история коммитов на гитхабе [1].
Перед тем как приступить к работе скажите "Привет" в нашем чате [2], 99% проблем с которыми вы можете столкнутся там решаются почти мгновенно.
Нам понадобится Composer [3], после его установки запускаем:
php composer.phar create-project phpixie/project
Это создаст папку project с скелетом проекта и одним бандлом 'app'. Бандлы это Бандлы это модули код, шаблоны, CSS итд. относящиеся к какой-то части приложения. Их можно легко переносить с проекта на проект используя Composer. Мы будем работать только с одним бандлом в котором и будет вся логика нашего приложения.
Дальше надо создать виртуальный хост и направит его на папку /web внутри проекта. Если все прошло гладко то зайдя на http://localhost/ [4] в браузере вы увидите приветствие. Сразу проверим работает ли роутинг перейдя на http://localhost/greet [5].
Если вы на Windows то скорее всего увидите ошибку во время запуска команды create-project, это следствия того что на этой ОС PHP функция symlink() не работает. Можете просто это проигнорировать, чуть потом я покажу как обойти эту проблему.
Состояние проекта на этом этапе (Коммит 1) [6]
Начнем с соединения с БД, для этого редактируем /assets/config/database.php. Проверить соединение можно запуском двух консольных команд с папки проекта:
./console framework:database drop # удаляет базу если она присутствует
./console framework:database create # создает базу если она отсутсвует
Дальше создаем миграцию со структурой таблиц в /assets/migrate/migrations/1_users_and_messages.sql:
CREATE TABLE users(
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE,
passwordHash VARCHAR(255)
);
-- statement
CREATE TABLE messages(
id INT PRIMARY KEY AUTO_INCREMENT,
userId INT NOT NULL,
text VARCHAR(255) NOT NULL,
date DATETIME NOT NULL,
FOREIGN KEY (userId)
REFERENCES users(id)
);
Заметьте что мы используем -- statement
для разделения запросов.
Также сразу добавим немного данных чтобы было чем наполнить базу, для этого создаем файлы в /assets/migrate/seeds/ где имя файла отвечает имени таблицы, например:
<?php
// /assets/migrate/seeds/messages.php
return [
[
'id' => 1,
'userId' => 1,
'text' => "Hello World!",
'date' => '2016-12-01 10:15:00'
],
// ....
]
Полный контент этих файлов можно посмотреть на гитхабе. Теперь запустим еще две консольные команды:
./console framework:migrate # применить миграции
./console framework:seed # наполнить базу данными
Теперь можно приступить к нашей первой странице. Сперва рассмотрим файл /bundles/app/assets/config/routeResolver.php в котором настраиваются роуты, то есть прописывается каким ссылкам отвечают какие процессоры. Мы собираемся добавить процессор messages который будет отвечать за отображение сообщений. Пропишем его как дефолтный а также сразу добавим роут для главной страницы:
return array(
'type' => 'group',
'defaults' => array('action' => 'default'),
'resolvers' => array(
'action' => array(
'path' => '<processor>/<action>'
),
'processor' => array(
'path' => '(<processor>)',
'defaults' => array('processor' => 'messages')
),
// Роут для главной страницы
'frontpage' => array(
'path' => '',
'defaults' => ['processor' => 'messages']
)
)
);
Начнем верстку с того что изменим родительский шаблон /bundles/app/assets/template/layout.php и добавим к нему Bootstrap 4 и свой CSS.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Bootstrap 4 -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css">
<!-- Подключаем наш CSS, об этом чуть позже -->
<link rel="stylesheet" href="/bundles/app/main.css">
<!-- Если подшаблон не установил имя страницы то используем Quickstart -->
<title><?=$_($this->get('pageTitle', 'Quickstart'))?></title>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-toggleable-md navbar-light bg-faded">
<div class="container">
<!-- Ссылка на главную страницу -->
<a class="navbar-brand mr-auto" href="<?=$this->httpPath('app.frontpage')?>">Quickstart</a>
</div>
</nav>
<!-- Тут будет вставлено тело дочернего шаблона -->
<?php $this->childContent(); ?>
<!-- Bootstrap dependencies -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js"></script>
</body>
</html>
Где же создать файл main.css? Поскольку все нужные файлы лучше всего держать внутри бандла то это будет папка /bundles/app/web/. При создании проекта композером на эту папку автоматически создается симлинк с /bundles/app/web что делает эти файлы доступными с браузера. На Windows вместо создания ярлыка приходится копировать папку, что делает команда:
# копирует файлы с web директории бандла в /web/bundles
./console framework:installWebAssets --copy
Теперь создаем новый процессор в /bundles/app/src/HTTP/Messages.php
namespace ProjectAppHTTP;
use PHPixieHTTPRequest;
/**
* Просмотр сообщений
*/
class Messages extends Processor
{
/**
* @param Request $request HTTP request
* @return mixed
*/
public function defaultAction($request)
{
$components = $this->components();
// Получаем все сообщения
$messages = $components->orm()->query('message')
->orderDescendingBy('date')
->find();
// Рендерим темплейт
return $components->template()->get('app:messages', [
'messages' => $messages
]);
}
}
Важно: не забываем прописать его в /bundles/app/src/HTTP.php:
namespace ProjectApp;
class HTTP extends PHPixieDefaultBundleHTTP
{
// это маппинг имени процессора к его классу
protected $classMap = array(
'messages' => 'ProjectAppHTTPMessages'
);
}
Почти готово, осталось только наверстать сам шаблон app:messages который использует процессор, это самая простая часть:
<?php
// Родительский шаблон
$this->layout('app:layout');
// Устанавливаем переменную какую
// родительский шаблон затем вставит как титул страницы
$this->set('pageTitle', "Messages");
?>
<div class="container content">
<-- Выводим сообщения -->
<?php foreach($messages as $message): ?>
<blockquote class="blockquote">
<-- Выводить текст надо используя $_() для защиты от XSS -->
<p class="mb-0"><?=$_($message->text)?></p>
<footer class="blockquote-footer">
posted at <?=$this->formatDate($message->date, 'j M Y, H:i')?>
</footer>
</blockquote>
<?php endforeach; ?>
</div>
Все, готово, теперь перейдя на http://localhost/ [4] мы увидим полный список сообщений.
Состояние проекта на этом этапе (Коммит 2) [7]
Для того чтобы под каждым сообщением указать пользователя который его создал надо прописать связь между таблицами. В миграциях мы указали что каждое сообщение включает обязательное поле userId так что это будет связь Один-ко-Многим.
// bundles/app/assets/config/orm.php
return [
'relationships' => [
// У каждого пользователя несколько сообщений
[
'type' => 'oneToMany',
'owner' => 'user',
'items' => 'message'
]
]
];
Добавим новый роут с параметром page для разбиения сообщений по страницам:
// /bundles/app/assets/config/routeResolver.php
return array(
// ....
'resolvers' => array(
'messages' => array(
'path' => 'page(/<page>)',
'defaults' => ['processor' => 'messages']
),
// ....
)
);
И чуть чуть меняем сам процессор Messages:
public function defaultAction($request)
{
$components = $this->components();
// Создаем запрос
$messageQuery = $components->orm()->query('message')
->orderDescendingBy('date');
// Передаем запрос в пейджер и сразу указываем количество
// сообщений на страницу и список связей которые надо подгрузить
$pager = $components->paginateOrm()
->queryPager($messageQuery, 10, ['user']);
// Выставляем номер текущей страницы исходя из параметра
$page = $request->attributes()->get('page', 1);
$pager->setCurrentPage($page);
// И рендерим темплейт
return $components->template()->get('app:messages', [
'pager' => $pager
]);
}
Теперь в шаблоне мы можем использовать $pager->getCurrentItems()
чтобы получить сообщения на данной странице, и $message->user()
чтобы получить данные об авторе и наверстать пейджер. Не буду копировать сюда полный шаблон страницы, его можно посмотреть в репозитории.
Состояние проекта на этом этапе (Коммит 3) [8]
Перед тем как позволить пользователям писать свои сообщения надо их авторизировать. Для этого надо указать и расширить сущность пользователя и его репозиторий. Тут важно понять отличие что сущность(Entity) представляет одного пользователя я репозиторий предоставляет методы поиска и создания этих сущностей. Для авторизации по паролю нам надо имплементировать несколько интерфейсов, все это довольно просто.
// /bundles/app/src/ORM/User.php
namespace ProjectAppORM;
use ProjectAppORMModelEntity;
/** Этот интерфейс необходим для логина по паролю */
use PHPixieAuthLoginRepositoryUser as LoginUser;
/**
* Сущность пользователя
*/
class User extends Entity implements LoginUser
{
/**
* Возвращает хеш пароля этого пользователя.
* В нашем случае это просто значение поля 'passwordHash'.
* @return string|null
*/
public function passwordHash()
{
return $this->getField('passwordHash');
}
}
namespace ProjectAppORMUser;
use ProjectAppORMModelRepository;
use ProjectAppORMUser;
/** Этот интерфейс необходим для логина по паролю */
use PHPixieAuthLoginRepository as LoginUserRepository;
/**
* Репозиторий пользователей
*/
class UserRepository extends Repository implements LoginUserRepository
{
/**
* Ищет пользователя по его id
* @param mixed $id
* @return User|null
*/
public function getById($id)
{
return $this->query()
->in($id)
->findOne();
}
/**
* Ищет пользователя по логину, в нашем случае это его email.
* Но можно искать и по нескольким полям в результате позволяя логинится
* и по мейлу и по имени юзера.
* @param mixed $login
* @return User|null
*/
public function getByLogin($login)
{
return $this->query()
->where('email', $login)
->findOne();
}
}
Важно: не забываем зарегистрировать эти классы в /bundles/app/src/ORM.php
namespace ProjectApp;
/**
* Тут мы прописываем классы врапперов
*/
class ORM extends PHPixieDefaultBundleORM
{
protected $entityMap = array(
'user' => 'ProjectAppORMUser'
);
protected $repositoryMap = [
'user' => 'ProjectAppORMUserUserRepository'
];
}
Пропишем настройки авторищации в /assets/config/auth.php:
// /assets/config/auth.php
return [
'domains' => [
'default' => [
// использовать ORM репозиторий для пользователей
'repository' => 'framework.orm.user',
// Тут мы настраиваем какими способами юзер может авторизироватся
'providers' => [
// Включаем поддержку сессий
'session' => [
'type' => 'http.session'
],
// И паролей
'password' => [
'type' => 'login.password',
// когда пользователь логинится паролем, запомнить его в сессии
'persistProviders' => ['session']
]
]
]
]
];
Осталось только добавить страницу логина, для этого создаем новый процессор:
namespace ProjectAppHTTP;
use PHPixieAuthLoginProvidersPassword;
use PHPixieHTTPRequest;
use PHPixieValidateForm;
use ProjectAppORMUserUserRepository;
use PHPixieAppORMUser;
/**
* Тут будем обрабатывать логин и регистрацию
*/
class Auth extends Processor
{
/**
* @param Request $request HTTP request
* @return mixed
*/
public function defaultAction($request)
{
// Если пользователь уже залогинен, редиректим его на главную
if($this->user()) {
return $this->redirect('app.frontpage');
}
$components = $this->components();
// Строим шаблон и форму
$template = $components->template()->get('app:login', [
'user' => $this->user()
]);
$loginForm = $this->loginForm();
$template->loginForm = $loginForm;
// Если форма не засабмичена то просто рендерим темплейт
if($request->method() !== 'POST') {
return $template;
}
$data = $request->data();
// В другом случае обрабатываем логин
$loginForm->submit($data->get());
// Если форма логина валидна и пользователь успешно залогинился делаем редирект
if($loginForm->isValid() && $this->processLogin($loginForm)) {
return $this->redirect('app.frontpage');
}
// Если нет то просто рендерим страницу
return $template;
}
/**
* Обработка логина
*
* @param Form $loginForm
* @return bool Залогинился ли пользователь
*/
protected function processLogin($loginForm)
{
// Пробуем залогинится
$user = $this->passwordProvider()->login(
$loginForm->email,
$loginForm->password
);
// Если пароль не подошел или такого пользователя нет, то добавляем ошибку к форме
if($user === null) {
$loginForm->result()->addMessageError("Invalid email or password");
return false;
}
return true;
}
/**
* Логаут
* @return mixed
*/
public function logoutAction()
{
// Получаем домен авторизации и забываем пользователя
$domain = $this->components()->auth()->domain();
$domain->forgetUser();
// Делаем редирект на главную
return $this->redirect('app.frontpage');
}
/**
* Строим форму логина
* @return Form
*/
protected function loginForm()
{
$validate = $this->components()->validate();
$validator = $validate->validator();
// Используем валидатор документов
//(это тот который вы будете использовать в большинстве случаев)
$document = $validator->rule()->addDocument();
// Оба поля обязательны
$document->valueField('email')
->required("Email is required");
$document->valueField('password')
->required("Password is required");
// Возвращаем форму для этого валидатора
return $validate->form($validator);
}
/**
* провайдер аутентификации какой мы настроили в /assets/config/auth.php
* @return Password
*/
protected function passwordProvider()
{
$domain = $this->components()->auth()->domain();
return $domain->provider('password');
}
}
Осталось только наверстать саму форму авторизации, чтобы не копировать сюда весь код, приведу пример одного поля:
<-- Добавить класс has-danger если поле не валидно -->
<div class="form-group <?=$this->if($loginForm->fieldError('email'), "has-danger")?>">
<-- Само поле ввода с сохранением предыдущего значения -->
<input name="email" type="text" value="<?=$_($loginForm->fieldValue('email'))?>"
class="form-control" placeholder="Username">
<-- Вывод ошибки если она есть -->
<?php if($error = $loginForm->fieldError('email')): ?>
<div class="form-control-feedback"><?=$error?></div>
<?php endif;?>
</div>
Так же добавляем роуты и ссылки на логин/логаут в хедер и готово, логин работает.
Состояние проекта на этом этапе (Коммит 4) [9]
Форма регистрации делается по полной аналогии, рассмотрим изменения к процессору Auth:
/**
* форма регистрации
* @return Form
*/
protected function registerForm()
{
$validate = $this->components()->validate();
$validator = $validate->validator();
$document = $validator->rule()->addDocument();
// По умолчанию валидатор не пропускает поля которые не были описаны.
// Этот вызов отключает эту проверку и пропускает дополнительные поля.
// В нашем случае это hidden поле "register" по какому мы будем определять
// логин это или регистрация
$document->allowExtraFields();
// Имя обязательное
$document->valueField('name')
->required("Name is required")
->addFilter()
->minLength(3)
->message("Username must contain at least 3 characters");
// Email тоже обязательный и должен быть валидным
$document->valueField('email')
->required("Email is required")
->filter('email', "Please provide a valid email");
// Обязательный и минимум 8 знаков
$document->valueField('password')
->required("Password is required")
->addFilter()
->minLength(8)
->message("Password must contain at least 8 characters");
// Тоже обязательное поле
$document->valueField('passwordConfirm')
->required("Please repeat your password");
// В этом коллбеке проверяем что пароль и его подтверждение совпадают
$validator->rule()->callback(function($result, $value) {
// Если они разные добавляем ошибку
if($value['password'] !== $value['passwordConfirm']) {
$result->field('passwordConfirm')->addMessageError("Passwords don't match");
}
});
// Строим форму для этого валидатора
return $validate->form($validator);
}
/**
* Обработка регистрации
* @param Form $registerForm
* @return bool Прошла ли регистрация успешно
*/
protected function processRegister($registerForm)
{
/** @var UserRepository $userRepository */
$userRepository = $this->components()->orm()->repository('user');
// Если email уже занят то добавляем ошибку к форме
if($userRepository->getByLogin($registerForm->email)) {
$registerForm->result()->field('email')->addMessageError("This email is already taken");
return false;
}
// Хешируем пароль и создаем пользователя
$provider = $this->passwordProvider();
$user = $userRepository->create([
'name' => $registerForm->name,
'email' => $registerForm->email,
'passwordHash' => $provider->hash($registerForm->password)
]);
$user->save();
// Вручную его логиним
$provider->setUser($user);
return true;
}
Единственное что надо заметить это то что мы добавили скрытое поле register
в HTML код формы, по какому проверяем логин это или регистрация.
Состояние проекта на этом этапе (Коммит 5) [10]
Теперь подключим логин из Facebook и Twitter. Начнем с того что добавим два поля facebookId
и twitterId
к таблице пользователей создав новую миграцию:
/* /assets/migrate/migrations/2_social_login.sql */
ALTER TABLE users ADD COLUMN twitterId VARCHAR(255) AFTER passwordHash;
-- statement
ALTER TABLE users ADD COLUMN facebookId VARCHAR(255) AFTER twitterId;
Теперь нам надо создать приложение на этих платформах и получить appId
и appSecret
. При регистрации правильно указываем Callback Url: http://localhost.com/socialAuth/callback/twitter
для Twitter и http://localhost.com/socialAuth/callback/twitter
для Facebook. Сами эти роуты ми создадим позже, а пока пропишем настройки:
// /assets/config/social.php
return [
'facebook' => [
'type' => 'facebook',
'appId' => 'YOUR APP ID',
'appSecret' => 'YOUR APP SECRET'
],
'twitter' => [
'type' => 'twitter',
'consumerKey' => 'YOUR APP ID',
'consumerSecret' => 'YOUR APP SECRET'
]
];
И включим поддержку социального логина в уже знакомом нам конфиге auth.php
:
// /assets/config/auth.php
<?php
return [
'domains' => [
'default' => [
// ....
'providers' => [
//.....
// Включаем соцлогин
'social' => [
'type' => 'social.oauth',
// После логина запоминаем пользователя в сессию
'persistProviders' => ['session']
]
]
]
]
];
Все, с настройками закончили, возьмемся за код. Помните как нам надо было имплементировать интерфейс в классах репозитории и сущности пользователя для логина по паролю? Теперь туда добавится еще один:
namespace ProjectAppORMUser;
// ....
/** Этот интерфейс позволяет логинится через соцсети */
use PHPixieAuthSocialRepository as SocialRepository;
class UserRepository extends Repository implements LoginUserRepository, SocialRepository
{
// ....
/**
* Находит пользователя по его данным полученным из компонента Social.
* Если пользователь не нашелся то возвращает null.
*
* @param SocialUser $socialUser
* @return User|null
*/
public function getBySocialUser($socialUser)
{
// Получаем имя поле в базе в котором хранится социальное id пользователя,
// например twitterId or facebookId
$providerName = $socialUser->providerName();
$field = $this->socialIdField($providerName);
// И делаем поиск по этому полю
return $this->query()->where($field, $socialUser->id())->findOne();
}
/**
* Получает имя поля с социальным id.
* В нашем случае ми делаем это просто добавляя 'Id' к имени провайдера
*
* @param string $providerName
* @return string
*/
public function socialIdField($providerName)
{
return $providerName.'Id';
}
}
И теперь наконец сам новый процессор для авторизации:
namespace ProjectAppHTTPAuth;
use PHPixieAppORMUser;
use PHPixieAuthSocialProvidersOAuth as OAuthProvider;
use PHPixieHTTPRequest;
use ProjectAppORMUserUserRepository;
use ProjectAppHTTPProcessor;
use PHPixieSocialOAuthUser as SocialUser;
/**
* Обрабатываем логин через соцсети
*/
class Social extends Processor
{
/**
* Переводит пользователя на внешнюю страницу логина
* например Twitter или Facebook
*
* @param Request $request HTTP request
* @return mixed
*/
public function defaultAction($request)
{
$provider = $request->attributes()->get('provider');
// Если параметр провайдера пуст, то делаем редирект на страницу логина
if(empty($provider)) {
return $this->redirect('app.processor', ['processor' => 'auth']);
}
// Создаем URL на внешнюю страницу и делаем редирект
$callbackUrl = $this->buildCallbackUrl($provider);
$url = $this->oauthProvider()->loginUrl($provider, $callbackUrl);
return $this->responses()->redirect($url);
}
/**
* Обрабатываем коллбек от провайдера.
* Это и будет та страница http://localhost.com/socialAuth/callback/twitter
* которую мы указали при регистрации нашего приложения.
*
* @param Request $request HTTP request
* @return mixed
*/
public function callbackAction($request)
{
$provider = $request->attributes()->getRequired('provider');
// Опять строим URL страницы коллбека, это нужно при получении токена
$callbackUrl = $this->buildCallbackUrl($provider);
$query = $request->query()->get();
// И вот сама обработка
// Если такой пользователь уже существует в базе то он сразу залогинится.
// В любом случае при успешной авторизации мы получим его соц данные в $userData
$userData = $this->oauthProvider()->handleCallback($provider, $callbackUrl, $query);
// Если что-то пошло не так, например пользователь отклонил авторизацию
// то редиректим его назад на страницу логина
if($userData === null) {
return $this->redirect('app.processor', ['processor' => 'auth']);
}
// Если же авторизация прошла успешно, но он гн залогинился
// значит это он первый раз на сайте и его надо зарегисторировать
if($this->user() === null) {
$user = $this->registerNewUser($userData);
// И после регистрации сразу залогинить
$this->oauthProvider()->setUser($user);
}
// Теперь он точно залогинен, так что возвращаем его на главную страницу
return $this->redirect('app.frontpage');
}
/**
* Регистрация новго пользователя из соцсети
*
* @param SocialUser $socialUser
* @return mixed
*/
protected function registerNewUser($socialUser)
{
/** @var UserRepository $userRepository */
$userRepository = $this->components()->orm()->repository('user');
// Получаем имя пользователя с его соц данных.
// Поскольку у разных провайдеров это поле может быть разное,
// то для этого создаем отдельный метод.
$profileName = $this->getProfileName($socialUser);
// Получаем имя поля куда сохранит его соц id
$socialIdField = $userRepository->socialIdField($socialUser->providerName());
// И создаем пользователя
$user = $userRepository->create([
'name' => $profileName,
$socialIdField => $socialUser->id()
]);
$user->save();
return $user;
}
/**
* Получам имя пользователя из его соц данных
*
* @param SocialUser $socialUser
* @return mixed
*/
protected function getProfileName($socialUser)
{
// В нашем у Twitter и Facebook поле имени одинаковое, так что все просто.
return $socialUser->loginData()->name;
}
/**
* Строим URL для коллбека, куда соцсеть
* перенаправит к нам пользователя после авторизации.
*
* @param $provider
* @return string
*/
protected function buildCallbackUrl($provider)
{
return $this->frameworkHttp()->generateUri('app.socialAuthCallback', [
'provider' => $provider
])->__toString();
}
/**
* Получаем проавайдер OAuth авторизации
*
* @return OAuthProvider
*/
protected function oauthProvider()
{
$domain = $this->components()->auth()->domain();
return $domain->provider('social');
}
}
Дальше прописываем роуты и добавляем ссылки логина в нашу форму авторизации:
// /bundles/app/assets/templates/login.php
<?php $url = $this->httpPath('app.socialAuth', ['provider' => 'twitter']); ?>
<a class="btn btn-lg btn-primary btn-block" href="<?=$url?>">Login with Twitter</a>
<?php $url = $this->httpPath('app.socialAuth', ['provider' => 'facebook']); ?>
<a class="btn btn-lg btn-primary btn-block" href="<?=$url?>">Login with Facebook</a>
Состояние проекта на этом этапе (Коммит 6) [11]
Тут собственно нет ничего интересного, еще одна форма, только в этот раз через AJAX для разнообразия. Тут единственное что надо отметить
так это использование блоков в шаблонах для добавления скриптов. И так мы добавляем блок scripts
в родительский шаблон:
<!-- /bundles/app/assets/templates/layout.php -->
<!-- Позволить подшаблонам добавлять свои скрипты в конец страницы -->
<?=$this->block('scripts')?>
Теперь в самом шаблоне messages
мы можем добавить скрипт в этот блок:
<!-- /bundles/app/assets/templates/messages.php -->
<?php $this->startBlock('scripts'); ?>
<script>
$(function() {
// Init the form handler
<?php $url = $this->httpPath('app.action', ['processor' => 'messages', 'action' => 'post']);?>
$('#messageForm').messageForm("<?=$_($url)?>");
});
</script>
<?php $this->endBlock(); ?>
В тот же блок можно добавить контент несколько раз, в результате он будет выведен в порядке добавления. Кстати можно сделать и наоборот,
добавлять контент только в том случае если блок пуст, рассмотрим два примера:
<?php $this->startBlock('test'); ?>
Hello
<?php $this->endBlock(); ?>
<?php $this->startBlock('test'); ?>
World
<?php $this->endBlock(); ?>
<?=$this->block('test')?>
<!-- Получим -->
Hello
World
<!--
Если второй параметр true и блок уже существует, то функция startBlock()
просто возвратит false а следовательно все что внутри if не выполнится.
-->
<?php if($this->startBlock('test', true)): ?>
Hello
<?php $this->endBlock();endif; ?>
<?php if($this->startBlock('test', true)): ?>
World
<?php $this->endBlock();endif; ?>
<?=$this->block('test')?>
<!-- Получим -->
Hello
Еще посмотрим что возвращает новый экшн процессора Messages
после того как было создано новое сообщение:
public function postAction($request)
{
// ....
// Возвратим ORM сущность как простой PHP объект.
// Параметр true так же указывает что надо рекурсивно превратить и подгруженные связи,
// но в данном случае их нет.
//
// А дальше PHPixie автоматически превращает объекты и массивы в JSON при выводе.
return $message->asObject(true);
}
**Состояние проекта на этом этапе (Коммит 7) [12]
Теперь добавим две консольные команды. Тут все по аналогии:
namespace ProjectAppConsole;
use PHPixieConsoleCommandConfig;
use PHPixieSliceData;
/**
* Листинг сообщений в консоли
*/
class Messages extends Command
{
/**
* Настройка команды
* @param Config $config
*/
protected function configure($config)
{
// Описание
$config->description("Print latest messages");
// Добавляем опцию для фильтра по id пользователя
$config->option('userId')
->description("Only print messages of this user");
// Добавляем аргумент для контроля количества сообщений
$config->argument('limit')
->description("Maximum number of messages to display, default is 5");
}
/**
* @param Data $argumentData
* @param Data $optionData
*/
public function run($argumentData, $optionData)
{
// считываем параметр количества
$limit = $argumentData->get('limit', 5);
// Строим запрос
$query = $this->components()->orm()->query('message')
->orderDescendingBy('date')
->limit($limit);
// Если указан userId то добавляем условие к запросу
$userId = $optionData->get('userId');
if($userId) {
$query->relatedTo('user', $userId);
}
// Получаем массив сообщений
$messages = $query->find(['user'])->asArray();
// Если их не нашлось
if(empty($messages)) {
$this->writeLine("No messages found");
}
// Выводим сообщения
foreach($messages as $message) {
$dateTime = new DateTime($message->date);
$this->writeLine($message->text);
$this->writeLine(sprintf(
"by %s on %s",
$message->user()->name,
$dateTime->format('j M Y, H:i')
));
$this->writeLine();
}
}
}
namespace ProjectAppConsole;
use PHPixieConsoleCommandConfig;
use PHPixieDatabaseDriverPDOConnection;
use PHPixieSliceData;
/**
* Выводит статистику по сообщениям
*/
class Stats extends Command
{
/**
* Настройка команды
* @param Config $config
*/
protected function configure($config)
{
$config->description("Display statistics");
}
/**
* @param Data $argumentData
* @param Data $optionData
*/
public function run($argumentData, $optionData)
{
// Получаем компонент Database
$database = $this->components()->database();
/** @var Connection $connection */
$connection = $database->get();
// Считаем все сообщения
$total = $connection->countQuery()
->table('messages')
->execute();
$this->writeLine("Total messages: $total");
// Получаем статистику по каждому пользователю
$stats = $connection->selectQuery()
->fields([
'name' => 'u.name',
// sqlExpression позволяет добавить сырой SQL
'count' => $database->sqlExpression('COUNT(1)'),
])
->table('messages', 'm')
->join('users', 'u')
->on('m.userId', 'u.id')
->groupBy('u.id')
->execute();
foreach($stats as $row) {
$this->writeLine("{$row->name}: {$row->count}");
}
}
}
Не забываем прописать их в классе ProjectAppConsole
:
namespace ProjectApp;
class Console extends PHPixieDefaultBundleConsole
{
/**
* Here we define console commands
* @var array
*/
protected $classMap = array(
'messages' => 'ProjectAppConsoleMessages',
'stats' => 'ProjectAppConsoleStats'
);
}
Готово, теперь посмотрим как они выглядят в консоли:
# ./console
Available commands:
app:messages Print latest messages
app:stats Display statistics
# ....
# ./console help app:messages
app:messages [ --userId=VALUE ] [ LIMIT ]
Print latest messages
Options:
userId Only print messages of this user
Arguments:
LIMIT Maximum number of messages to display, default is 5
# ./console help app:stats
app:stats
Display statistics
И сами результаты роботы:
# ./console app:messages 2
Simplicity is the ultimate sophistication. -- Leonardo da Vinci
by Trixie on 7 Dec 2016, 16:40
Simplicity is prerequisite for reliability. -- Edsger W. Dijkstra
by Trixie on 7 Dec 2016, 15:05
# ./console app:stats
Total messages: 14
Pixie: 3
Trixie: 11
Состояние проекта на этом этапе (Коммит 8) [13]
Для того чтобы все параметры зависящие от сервера отделить от самой конфигурации, можно использовать параметризацию.
Все очень просто, создаем файл /assets/parameters.php
в котором лежат параметры, а затем ссылаемся на них из конфигов.
// /assets/parameters.php
return [
'database' => [
'name' => 'phpixie',
'user' => 'phpixie',
'password' => 'phpixie'
],
'social' => [
'facebookId' => 'YOUR APP ID',
'facebookSecret' => 'YOUR APP SECRET',
'twitterId' => 'YOUR APP ID',
'twitterSecret' => 'YOUR APP SECRET',
]
];
И теперь изменяем сами конфиги:
// /assets/config/database.php
return [
// Database configuration
'default' => [
// Ссылки на параметры в /assets/parameters.php
'database' => '%database.name%',
'user' => '%database.user%',
'password' => '%database.password%',
'adapter' => 'mysql',
'driver' => 'pdo'
]
];
// /assets/config/social.php
return [
'facebook' => [
'type' => 'facebook',
'appId' => '%social.facebookId%',
'appSecret' => '%social.facebookSecret%'
],
'twitter' => [
'type' => 'twitter',
'consumerKey' => '%social.twitterId%',
'consumerSecret' => '%social.twitterSecret%'
]
];
Теперь при деплойменте на сервер достаточно только заменить этот один файл. Кстати поскольку это просто PHP код то в нем
можно использовать и привычные конструкции типа if
и switch
чтобы отдавать разные параметры в зависимости от сервера.
Финальный код [14]
Вот и все, у нас получился полностью функциональный сайт, надеюсь вам понравилось. Если вам интересно и вы хотите узнать больше то заходите к нам в чат, у нас всегда весело, даже если вы не используете сам фреймворк.
А если вы хотите помочь проекту то можете поставить нам звездочку на гитхабе :)
Автор: jigpuzzled
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/tutorial/237138
Ссылки в тексте:
[1] на гитхабе: https://github.com/PHPixie/Demo-Quickstart/commits/master
[2] чате: https://gitter.im/PHPixie/Hotline
[3] Composer: https://getcomposer.org/download/
[4] http://localhost/: http://localhost/
[5] http://localhost/greet: http://localhost/greet
[6] Состояние проекта на этом этапе (Коммит 1): https://github.com/PHPixie/Demo-Quickstart/tree/8702c5a5f732540d973770edb3604fa719aadef4
[7] Состояние проекта на этом этапе (Коммит 2): https://github.com/PHPixie/Demo-Quickstart/tree/361acb0dacfe5e3a89a58400420292dad2acbe3a
[8] Состояние проекта на этом этапе (Коммит 3): https://github.com/PHPixie/Demo-Quickstart/tree/a3cd3aa05d79db09b54a1a9ff49feba998d51a88
[9] Состояние проекта на этом этапе (Коммит 4): https://github.com/PHPixie/Demo-Quickstart/tree/92fc8e0e314a30424e2cfba616932c2d9a294faf
[10] Состояние проекта на этом этапе (Коммит 5): https://github.com/PHPixie/Demo-Quickstart/tree/7e3cf803c02f579ff6e29f6b64369d6ceb439970
[11] Состояние проекта на этом этапе (Коммит 6): https://github.com/PHPixie/Demo-Quickstart/tree/a34107cd33f0b56a249d299bd92788dac09d5294
[12] Состояние проекта на этом этапе (Коммит 7): https://github.com/PHPixie/Demo-Quickstart/tree/48edabf4d69ee848346432300d357e5e96656780
[13] Состояние проекта на этом этапе (Коммит 8): https://github.com/PHPixie/Demo-Quickstart/tree/df8d0e75aea57d0dfebf7deea23909b33513e352
[14] Финальный код: https://github.com/PHPixie/Demo-Quickstart
[15] Источник: https://habrahabr.ru/post/320056/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.