Новий швидкий старт з PHPixie: будуємо цитатник отримати за комітом

image
За минулий рік в PHPixie додалося багато нових можливостей і кілька компонентів, до того ж трохи змінилася стандартна структура пакету щоб знизити поріг входження для розробників. Так що настав час створити новий туторіал, і в цей раз ми спробуємо зробити його трохи по іншому. Замість того щоб просто дивитися на готовий демо проект з описом, ми будемо йти поступово, при чому на кожній ітерації у нас буде повністю робочий сайт. Ми будемо будувати простенький цитатник з логіном, реєстрацією, інтеграцією з соціальними мережами і консольними командами для статистики. Повна історія комітів на гітхабі.
1. Створення проекту
Перед тим як приступити до роботи скажіть "Привіт" у нашому чаті, 99% проблем з якими ви можете зіткнутися там вирішуються майже миттєво.
Нам знадобиться Composer, після його установки запускаємо:
php composer.phar create-project phpixie/project

Це створить папку project з скелетом проекту і одним бандлом 'app'. Бандли це Бандли це модулі код, шаблони, CSS ітд. відносяться до якоїсь частини програми. Їх можна легко переносити з проекту на проект використовуючи Composer. Ми будемо працювати тільки з одним бандлом в якому і буде вся логіка нашої програми.
Далі треба створити віртуальний хост і направить його на папку /web всередині проекту. Якщо все пройшло гладко то зайшовши на http://localhost/ у браузері ви побачите привітання. Відразу перевіримо працює роутинг перейшовши на http://localhost/greet.
Якщо ви на Windows то швидше за все побачите помилку під час запуску команди create-project, це наслідки того, що на цій ОС PHP функція symlink() не працює. Можете просто проігнорувати, трохи потім я покажу як обійти цю проблему.
Стан проекту на цьому етапі (Комміт 1)
2. Перегляд повідомлень
Почнемо з з'єднання з БД, для цього редагуємо /assets/config/database.php. Перевірити з'єднання можна запуском двох консольних команд з папки проекту:
./console framework:drop database # видаляє базу якщо вона присутня
./console framework:create database # створює базу якщо вона відсутня

Далі створюємо міграцію із структурою таблиць у /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/насіння/ де ім'я файлу відповідає ім'я таблиці, наприклад:
<?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 Project\App\HTTP;

use PHPixie\HTTP\Request;

/**
* Перегляд повідомлень
*/
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 Project\App;

class HTTP extends \PHPixie\DefaultBundle\HTTP
{
// це маппінг імені процесора до його класу
protected $classMap = array(
'messages' => 'Project\App\HTTP\Messages'
);
}

Майже готове, залишилося тільки надолужити сам шаблон 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/ ми побачимо повний список повідомлень.
Стан проекту на цьому етапі (Комміт 2)
3. ORM зв'язку і розбиття на сторінки
Для того щоб під кожним повідомленням вказати користувача, який його створив треба прописати зв'язок між таблицями. У міграціях ми вказали що кожне повідомлення включає обов'язкове поле 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)
4. Авторизація користувачів
Перед тим як дозволити користувачам писати свої повідомлення треба їх авторизувати. Для цього треба вказати і розширити сутність користувача і його репозиторій. Тут важливо зрозуміти відмінність що сутність(Entity) являє одного користувача я репозиторій надає методи пошуку і створення цих сутностей. Для авторизації по паролю нам треба імплементувати кілька інтерфейсів, все це досить просто.
// /bundles/app/src/ORM/User.php

namespace Project\App\ORM;

use Project\App\ORM\Model\Entity;
/** Цей інтерфейс необхідний для логіну за паролем */
use PHPixie\AuthLogin\Repository\User as LoginUser;

/**
* Сутність користувача
*/
class User extends Entity implements LoginUser
{
/**
* Повертає хеш пароля користувача.
* У нашому випадку це просто значення поля 'passwordHash'.
* @return string|null
*/
public function passwordHash()
{
return $this->getField('passwordHash');
}
}

namespace Project\App\ORM\User;

use Project\App\ORM\Model\Repository;
use Project\App\ORM\User;
/** Цей інтерфейс необхідний для логіну за паролем */
use PHPixie\AuthLogin\Repository 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 Project\App;

/**
* Тут ми прописуємо класи врапперов
*/
class ORM extends \PHPixie\DefaultBundle\ORM
{
protected $entityMap = array(
'user' => 'Project\App\ORM\User'
);

protected $repositoryMap = [
'user' => 'Project\App\ORM\User\UserRepository'
];
}

Пропишемо налаштування авторищации /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 Project\App\HTTP;

use PHPixie\AuthLogin\Providers\Password;
use PHPixie\HTTP\Request;
use PHPixie\Validate\Form;
use Project\App\ORM\User\UserRepository;
use PHPixie\App\ORM\User;

/**
* Тут будемо обробляти логін та реєстрацію
*/
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)
5. Реєстрація
Форма реєстрації робиться за повною аналогією, розглянемо зміни до процесора 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("repeat Please 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)
6. Соціальний логін
Тепер підключимо логін з 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 Project\App\ORM\User;

/ / ....

/** Цей інтерфейс дозволяє логинится через соцмережі */
use PHPixie\AuthSocial\Repository 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 Project\App\HTTP\Auth;

use PHPixie\App\ORM\User;
use PHPixie\AuthSocial\Providers\OAuth as OAuthProvider;
use PHPixie\HTTP\Request;
use Project\App\ORM\User\UserRepository;
use Project\App\HTTP\Processor;
use PHPixie\Social\OAuth\User 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)
7. Додавання повідомлень
Тут, власне, немає нічого цікавого, ще одна форма, тільки в цей раз через 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)
8. Консольні команди
Тепер додамо дві консольні команди. Тут все по аналогії:
namespace Project\App\Console;

use PHPixie\Console\Command\Config;
use PHPixie\Slice\Data;
/**
* Лістинг повідомлень в консолі
*/
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 Project\App\Console;

use PHPixie\Console\Command\Config;
use PHPixie\Database\Driver\PDO\Connection;
use PHPixie\Slice\Data;

/**
* Виводить статистику за повідомленнями
*/
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}");
}
}
}

Не забуваємо прописати їх у класі
Project\App\Console
:
namespace Project\App;

class Console extends \PHPixie\DefaultBundle\Console
{
/**
* Here we define console commands
* @var array
*/
protected $classMap = array(
'messages' => 'Project\App\Console\Messages',
'stats' => 'Project\App\Console\Stats'
);
}

Готово, тепер подивимося, як вони виглядають в консолі:
# ./console

Available commands:

app:Print messages 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)
9. Використання параметрів конфігурації
Для того щоб усі параметри залежать від сервера відокремити від самої конфігурації, можна використовувати параметризацію.
Все дуже просто, створюємо файл
/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
щоб віддавати різні параметри в залежності від сервера.
Фінальний код
Кінець
Ось і все, у нас вийшов повністю функціональний сайт, сподіваюся вам сподобалося. Якщо вам цікаво і ви хочете дізнатися більше то заходьте до нас в чат, у нас завжди весело, навіть якщо ви не використовуєте сам фреймворк.
А якщо ви хочете допомогти проекту, то можете поставити нам зірочку на гітхабі :)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.