- PVSM.RU - https://www.pvsm.ru -

Новый быстрый старт с PHPixie: строим цитатник коммит за коммитом

image

За прошлый год в PHPixie добавилось много новых возможностей и несколько компонентов, к тому же немного изменилась стандартная структура бандла чтобы снизить порог вхождения для разработчиков. Так что пришло время создать новый туториал, и в этот раз мы попробуем сделать его чуть по другому. Вместо того чтобы просто смотреть на готовый демо проект с описанием, мы будем идти постепенно, при чем на каждой итерации у нас будет полностью рабочий сайт. Мы будем строить простенький цитатник с логином, регистрацией, интеграцией с соцсетями и консольными командами для статистики. Полная история коммитов на гитхабе [1].

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]

2. Просмотр сообщений

Начнем с соединения с БД, для этого редактируем /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]

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) [8]

4. Авторизация пользователей

Перед тем как позволить пользователям писать свои сообщения надо их авторизировать. Для этого надо указать и расширить сущность пользователя и его репозиторий. Тут важно понять отличие что сущность(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]

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("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]

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 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]

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) [12]

8. Консольные команды

Теперь добавим две консольные команды. Тут все по аналогии:

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]

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 чтобы отдавать разные параметры в зависимости от сервера.

Финальный код [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