⬆️ ⬇️

New quick start with PHPixie: we build quote-by-commit commit

image Over the past year, many new features and several components have been added to PHPixie, and the standard bundle structure has slightly changed to lower the entry threshold for developers. So it's time to create a new tutorial, and this time we will try to make it a little different. Instead of just looking at the finished demo project with a description, we will go gradually, and at each iteration we will have a fully working site. We will build a simple quote pad with login, registration, integration with social networks and console commands for statistics. The complete github history .



1. Creating a project



Before you start work, say "Hello" in our chat , 99% of the problems that you may encounter there are solved almost instantly.

We will need Composer , after its installation we run:



php composer.phar create-project phpixie/project 


This will create a project folder with the project skeleton and one 'app' bundle. Bundles are modules code, templates, CSS, etc. related to some part of the application. They can be easily transferred from project to project using Composer. We will work with only one bundle in which all the logic of our application will be.



Next you need to create a virtual host and direct it to the / web folder inside the project. If everything went smoothly, going to http: // localhost / in the browser you will see a greeting. Immediately check whether the routing works by going to http: // localhost / greet .



If you are on Windows, you will most likely see an error when you run the create-project command, this is a consequence of the fact that symlink () does not work on this PHP OS. You can just ignore it, a little later I'll show you how to get around this problem.



Project status at this stage (Commit 1)



2. View messages



Let's start with the database connection, for this we edit /assets/config/database.php . You can check the connection by running two console commands from the project folder:



 ./console framework:database drop #      ./console framework:database create #      


Next, create a migration with the table structure in /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) ); 


Notice that we use -- statement to separate requests.



We also immediately add some data to fill the database with something, for this we create files in / assets / migrate / seeds / where the file name corresponds to the table name, for example:



 <?php // /assets/migrate/seeds/messages.php return [ [ 'id' => 1, 'userId' => 1, 'text' => "Hello World!", 'date' => '2016-12-01 10:15:00' ], // .... ] 


The full content of these files can be viewed on the githab. Now we will launch two more console commands:



 ./console framework:migrate #   ./console framework:seed #    


Now you can proceed to our first page. First, we look at the /bundles/app/assets/config/routeResolver.php file in which the routes are configured, that is, it specifies which links correspond to which processors. We are going to add a messages processor which will be responsible for displaying messages. Let's write it as default and also immediately add a route for the main page:



 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'] ) ) ); 


Let's start the layout by changing the parent template /bundles/app/assets/template/layout.php and adding Bootstrap 4 and your CSS to it.



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


Where to create the main.css file? Since all the necessary files are best kept inside the bundle, this will be the / bundles / app / web / folder. When you create a project with a composite to this folder, a simlink with / bundles / app / web is automatically created, which makes these files accessible from the browser. On Windows, instead of creating a shortcut, you have to copy the folder, which the command does:



 #    web    /web/bundles ./console framework:installWebAssets --copy 


Now create a new processor in /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 ]); } } 


Important: do not forget to register it in /bundles/app/src/HTTP.php :



 namespace Project\App; class HTTP extends \PHPixie\DefaultBundle\HTTP { //        protected $classMap = array( 'messages' => 'Project\App\HTTP\Messages' ); } 


Almost done, it remains only to make up the app: messages template itself which uses the processor, this is the simplest part:



 <?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 MY, H:i')?> </footer> </blockquote> <?php endforeach; ?> </div> 


Everything is ready, now going to http: // localhost / we will see the full list of messages.



Project status at this stage (Commit 2)



3. ORM communication and pagination



In order to specify the user who created it under each message, it is necessary to prescribe the relationship between the tables. In the migrations, we indicated that each message includes a mandatory userId field so that it will be a One-to-Many relationship.



 // bundles/app/assets/config/orm.php return [ 'relationships' => [ //      [ 'type' => 'oneToMany', 'owner' => 'user', 'items' => 'message' ] ] ]; 


Add a new route with the page parameter for splitting messages by pages:



 // /bundles/app/assets/config/routeResolver.php return array( // .... 'resolvers' => array( 'messages' => array( 'path' => 'page(/<page>)', 'defaults' => ['processor' => 'messages'] ), // .... ) ); 


And we slightly change the Messages processor itself:



 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 ]); } 


Now in the template we can use $pager->getCurrentItems() to get the messages on this page, and $message->user() to get the data about the author and catch up with the pager. I will not copy the full page template here, it can be viewed in the repository.



Project status at this stage (Commit 3)



4. User authorization



Before you allow users to write their messages, you need to authorize them. To do this, you need to specify and expand the user's identity and its repository. Here it is important to understand the difference that an entity (Entity) represents a single user; the repository provides methods for searching and creating these entities. For password authorization, we need to implement several interfaces, all this is quite simple.



 // /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(); } } 


Important: don't forget to register these classes in /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' ]; } 


Let's write the authorization settings in /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'] ] ] ] ] ]; 


It remains only to add a login page, for this we create a new processor:



 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'); } } 


It remains only to make up the authorization form itself, so as not to copy all the code here, I will give an example of one field:



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


We also add routes and links to login / logout to the header and ready, login works.



Project status at this stage (commit 4)



5. Registration



The registration form is done in full analogy, consider the changes to the Auth processor:



 /** *   * @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; } 


The only thing that should be noted is that we have added a hidden register field to the HTML code of the form, by which we check the login or registration.



Project status at this stage (commit 5)



6. Social login



Now connect the login from Facebook and Twitter. Let's start by adding two facebookId and twitterId to the table of users by creating a new migration:



 /* /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; 


Now we need to create an application on these platforms and get appId and appSecret . When registering, we specify the Callback Url correctly: http://localhost.com/socialAuth/callback/twitter for Twitter and http://localhost.com/socialAuth/callback/twitter for Facebook. We will create these routes ourselves later, but for now let's set up the settings:



 // /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' ] ]; 


And we will include support for a social login in the auth.php config already known to us:



 // /assets/config/auth.php <?php return [ 'domains' => [ 'default' => [ // .... 'providers' => [ //..... //   'social' => [ 'type' => 'social.oauth', //       'persistProviders' => ['session'] ] ] ] ] ]; 


All finished with the settings, take up the code. Remember how we had to implement the interface in the repository and user entity classes for a username with a password? Now one more will be added there:



 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'; } } 


And now, finally, the new authorization processor itself:



 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'); } } 


Next we register the routes and add the login links to our authorization form:



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


Project status at this stage (Commit 6)



7. Adding messages



There is actually nothing interesting, another form, only this time through AJAX for a change. Here the only thing to be noted

so is the use of blocks in templates for adding scripts. And so we add the scripts block to the parent template:



 <!-- /bundles/app/assets/templates/layout.php --> <!--         --> <?=$this->block('scripts')?> 


Now, in the messages template itself, we can add a script to this block:



 <!-- /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 MY, 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: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)



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 .





the end



, , . , , .



:)



')

Source: https://habr.com/ru/post/320056/



All Articles