Skip to content

Instantly share code, notes, and snippets.

@dracony
Created January 23, 2017 12:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dracony/dfb5c81b9c2fe4ec88c193fb1f0304ef to your computer and use it in GitHub Desktop.
Save dracony/dfb5c81b9c2fe4ec88c193fb1f0304ef to your computer and use it in GitHub Desktop.
За прошлый год в PHPixie добавилось много новых возможностей и несколько компонентов, к тому же немного изменилась стандартная структура бандла чтобы снизить порог вхождения для разработчиков. Так что пришло время создать новый туториал, и в этот раз мы попробуем сделать его чуть по другому. Вместо того чтобы просто скинуть готовый демо проект с описанием, мы будем идти постепенно при чем на каждой итерации у нас будет полностью рабочий сайт. **Мы будем строить простенький цитатник с логином, регистрацией, интеграцией с соцсетями и консольными командами для статистики.** Полная история коммитов тут: .
<cut />
**1. Создание проекта**
Нам понадобится [Composer](https://getcomposer.org/download/), после его установки запускаем:
```
php composer.phar create-project phpixie/project
```
Это создаст папку *project* с скелетом проекта и одним бандлом 'app'. Бандлы это Бандлы это модули код, шаблоны, CSS итд. относящиеся к какой-то части приложения. Их можно легко переносить с проекта на проект используя Composer. Мы будем работать только с одним бандлом в котором и будет вся логика нашего приложения.
Дальше надо создать виртуальный хост и направит его на папку */web* внутри проекта. Если все прошло гладко то зайдя на *http://localhost/* в браузере вы увидите приветствие. Сразу проверим работает ли роутинг перейдя на *http://localhost/greet*.
Если вы на Windows то скорее всего увидите ошибку во время запуска команды *create-project*, это следствия того что на этой ОС PHP функция *symlink()* не работает. Можете просто это проигнорировать, чуть потом я покажу как обойти эту проблему.
**[Состояние проекта на этом этапе (Коммит 1)](https://github.com/PHPixie/Demo-Quickstart/tree/8702c5a5f732540d973770edb3604fa719aadef4)**
**2. Просмотр сообщений**
Начнем с соединения с БД, для этого редактируем */assets/config/database.php*. Проверить соединение можно запуском двух консольных команд с папки проекта:
```
./console framework:database drop # удаляет базу если она присутсвует
./console framework:database create # создает базу если она отсутсвует
```
Дальше создаем миграцию со структурой таблиц в */assets/migrate/migrations/1_users_and_messages.sql*:
```php
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
<?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* который будет отвечать за отображение сообщений. Пропишем его как дефолтный а также сразу добавим роут для главной страницы:
```php
<?php
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.
```php
<!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*
```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**:
```php
namespace Project\App;
class HTTP extends \PHPixie\DefaultBundle\HTTP
{
// это маппинг имени процессора к его классу
protected $classMap = array(
'messages' => 'Project\App\HTTP\Messages'
);
}
```
Почти готово, осталось только наверстать сам шаблон *app:messages* который использует процессор, это самая простая часть:
```php
<?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)](https://github.com/PHPixie/Demo-Quickstart/tree/361acb0dacfe5e3a89a58400420292dad2acbe3a)**
**3. ORM связи и пейджинация**
Для того чтобы под каждым сообщением указать пользователя который его создал надо прописать связь между таблицами. В миграциях мы указали что каждое сообщение включает обязательное поле *userId* так что это будет связь Один-ко-Многим.
```php
// bundles/app/assets/config/orm.php
return [
'relationships' => [
// У каждого пользователя несколько сообщений
[
'type' => 'oneToMany',
'owner' => 'user',
'items' => 'message'
]
]
];
```
Добавим новый роут с параметром *page* для разбиения сообщений по страницам:
```php
// /bundles/app/assets/config/routeResolver.php
return array(
// ....
'resolvers' => array(
'messages' => array(
'path' => 'page(/<page>)',
'defaults' => ['processor' => 'messages']
),
// ....
)
);
```
И чуть чуть меняем сам процессор Messages:
```php
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)](https://github.com/PHPixie/Demo-Quickstart/tree/a3cd3aa05d79db09b54a1a9ff49feba998d51a88)**
**4. Авторизация пользователей**
Перед тем как позволить пользователям писать свои сообщения надо их авторизировать. Для этого надо указать и расширить сущность пользователя и его репозиторий. Тут важно понять отличие что сущность(Entity) представляет одного пользователя я репозиторий предоставляет методы поиска и создания этих сущностей. Для авторизации по паролю нам надо имплементировать несколько интерфейсов, все это довольно просто.
```php
// /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');
}
}
```
```php
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**
```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*:
```php
// /assets/config/auth.php
return [
'domains' => [
'default' => [
// использовать ORM репозиторий для пользователей
'repository' => 'framework.orm.user',
// Тут мы настраиваем какими способами юзер может авторизироватся
'providers' => [
// Включаем поддержку сессий
'session' => [
'type' => 'http.session'
],
// И паролей
'password' => [
'type' => 'login.password',
// когда пользователь логинится паролем, запомнить его в сессии
'persistProviders' => ['session']
]
]
]
]
];
```
Осталось только добавить страницу логина, для этого создаем новый процессор:
```php
<?php
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');
}
}
```
Осталось только наверстать саму форму авторизации, чтобы не копировать сюда весь код, приведу пример одного поля:
```php
<-- Добавить класс 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)](https://github.com/PHPixie/Demo-Quickstart/tree/92fc8e0e314a30424e2cfba616932c2d9a294faf)**
5. Регистрация
Форма регистрации делается по полной аналогии, рассмотрим изменения к процессору Auth:
```php
/**
* форма регистрации
* @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 is required and must be a valid email
$document->valueField('email')
->required("Email is required")
->filter('email', "Please provide a valid email");
$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");
// In this callback rule we check that password confirmation matches the password
$validator->rule()->callback(function($result, $value) {
// If they don't match we add an error to the field
if($value['password'] !== $value['passwordConfirm']) {
$result->field('passwordConfirm')->addMessageError("Passwords don't match");
}
});
// Build a form for this validator
return $validate->form($validator);
}
/**
* Process registration
* @param Form $registerForm
* @return bool Whether the user was successfully registered
*/
protected function processRegister($registerForm)
{
/** @var UserRepository $userRepository */
$userRepository = $this->components()->orm()->repository('user');
// Check if the email already exists and if so add an error to the form
if($userRepository->getByLogin($registerForm->email)) {
$registerForm->result()->field('email')->addMessageError("This email is already taken");
return false;
}
// Hash password and create the user
$provider = $this->passwordProvider();
$user = $userRepository->create([
'name' => $registerForm->name,
'email' => $registerForm->email,
'passwordHash' => $provider->hash($registerForm->password)
]);
$user->save();
// Manually log the user in
$provider->setUser($user);
return true;
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment