Friday, August 28, 2015

Build a PHP website using Symfony 2 components and Bootstrap

After a short trip into Silex and Pimple I moved a step forward into understanding frameworks and design patterns. I decided to  build on shoulders of giants by using components from Symfony 2 framework.

My starting point was this tutorial: "http://www.sitepoint.com/build-php-framework-symfony-components/" You should read it first before reading the rest of this post. I followed this  tutorial with small exceptions:
   - added "public" folder where I store index.php and .htaccess
   - added "CPANA\Framework" folder to store Core.php and RequestEvent.php
   - added to the file "vendor/composer/autoload_psr4.php" the path to my files:  'CPANA\\Framework\\' =>array( $baseDir.'/CPANA/Framework'),


Organize the code further
- create structure for Controllers, Models, Views under CPANA directory
- add to vendor/composer/autoload_psr4.php path to my new folders:
    'CPANA\\Controllers\\' =>array( $baseDir.'/CPANA/Controllers'),
    'CPANA\\Models\\' =>array( $baseDir.'/CPANA/Models'),
    'CPANA\\Views\\' =>array( $baseDir.'/CPANA/Views')
,
NOTE!!! If you use composer to install other libraries it will overwrite the file autoload_psr4.php you need to paste again these values
- create basic controller class Pages under "Controllers" folder
- modified the index.php to map Pages::Home method with path '/'

        $app->map('/','CPANA\Controllers\Pages::Home');

!!!!!In order for call_usern_func_array() to be able to call the object method, it needs the fully qualified namesapce: http://stackoverflow.com/questions/14682356/relative-namespaces-and-call-user-func
---------------------------------------------------------------------------------------------------------------
--------------------  handling routes with parameters  ---------------------------------------------------
----------------------------------------------------------------------------------------------------------------
$app->map('/hello/{name}','CPANA\Controllers\Pages::Hello');

in Core.php

$matcher = new UrlMatcher($this->routes, $context);
       
        try {
            $attributes = $matcher->match($request->getPathInfo());


The match method tries to match the URL against a known route pattern, and returns the corresponding route attributes in case of success           
If we check what is stored in $attributes with var_dump($attributes) we can see:

array (size=3)
  'controller' => string 'CPANA\Controllers\Pages::Hello' (length=30)
  'name' => string 'Mela' (length=4)
  '_route' => string '/hello/{name}' (length=13)


 The parameters are taken in order by call_user_func_array function. The first one is "controller" but we do not need that, this is  why we will unset it. Now the first parameter in list is 'name' and our method:  Hello() accepts just one parameter, so it will work with the value stored in $attributes['name']. If we need also the second parameter (in this case '__route') we will add another
 parameter in our method declaration Hello($name, $second_param) (it doesn't matter the name just the order).

 In Core::handle()
 ............
 $matcher = new UrlMatcher($this->routes, $context);
       
        try {
            $attributes = $matcher->match($request->getPathInfo());
           
            $controller = $attributes['controller'];
           
            unset($attributes['controller']);
           
            $response = call_user_func_array($controller,$attributes);
 .....


 ---------------------------------------------------------------------------------------------------------------
 -------------------------- Basic  templates --------------------------------------------------------------------------
 ----------------------------------------------------------------------------------------------------------------
 under CPANA\Views added a folder called "shared" which should hold the general template of the website
 -added template.php and dumped HTML code inside
 -under CPANA\Views added view class called HomeView.php which contains one method render()

 public function render($msg)
    {   
        $content='';
       
        ob_start();
        $message=$msg;
        require 'shared/template.php';
        $content = ob_get_contents();
        ob_end_clean();
       
        return $content;
    }


 In the controller associated with '/' path (Pages::Home) I added:

 public static function Home()
    {   
        $msg="Pagina de bun venit"; //some content for home page
        $r=new Response();
        $content=new HomeView(); //I create an instance of the view
       
        $r->setContent($content->render($msg));
        return $r;
    }

--------------------------------------------------------------------------------------------------------------
---------- Symfony templating -------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------

Nice! I have now a template an a View class. Let's see how Symfony implements this features:

http://symfony.com/doc/current/components/templating/introduction.html

change content of the HomeView->render() method using the example from documentation:

use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\Loader\FilesystemLoader;

class HomeView
{
    public function render($msg)
    {   
   
        $loader = new FilesystemLoader(__DIR__.'/shared/%name%');
        $templating = new PhpEngine(new TemplateNameParser(), $loader);
       
        $content=$templating->render('template.php', array('message' => $msg));
       
        return $content;


It makes sense to move this logic in the Controller and for the moment we can delete HomeView.php, AboutView.php etc.
The controller will look like this:

class Pages
{
    public static function Home()
    {   
        $msg="Home page welcome";
        $r=new Response();
       
        $loader = new FilesystemLoader(dirname(__DIR__).'/Views/shared/%name%');
        $templating = new PhpEngine(new TemplateNameParser(), $loader);
       
        $content=$templating->render('template.php', array('message' => $msg));
               
        $r->setContent($content);
        return $r;
    }

--------------------------------------------------------------------
https://symfony-docs-chs.readthedocs.org/en/2.0/components/templating.html
http://symfony.com/doc/current/components/templating/helpers/slotshelper.html
- add   use Symfony\Component\Templating\Helper\SlotsHelper;
- delete folder shared add both template.php and home.view.php directly under /Views

I am storing a page title and a page content in $msg;
Following the instructions found in the 2 links above I am creating a FilesystemLoader and new PhpEngine object, also add SlotsHelper.
I am not using it yet, but there is also an AssetHelper (http://symfony.com/doc/current/components/templating/helpers/assetshelper.html)

public static function Home()
    {   
        $msg['content']="Pagina de bun venit";
        $msg['title']="Titlu baa";
        $r=new Response();
       
        //use symfony/templeting classes
        $loader = new FilesystemLoader(dirname(__DIR__).'/Views/%name%');
        $templating = new PhpEngine(new TemplateNameParser(), $loader);
        $templating->set(new SlotsHelper());

        // Retrieve $page object
        $content=$templating->render('home.view.php', array('page' => $msg));
               
        $r->setContent($content);
        return $r;
    }


The general template file is "template.php". I put inside the HTML body this code:
<?php
        // The _content slot is a special slot set by the PhpEngine. It contains the content of the subtemplate.
        $view['slots']->output('_content');
?>


The specific page view for path '/' is "home.view.php"
The extend() method is called in the sub-template to set its parent template. Then $view['slots']->set() can be used to set the content of a slot. All content which is not explicitly set in a slot is in the _content slot.

home.view.php

<?php $view->extend('template.php') ?>

<?php $view['slots']->set('title', $page['title']) ?>

<h1>
    <?php echo $page['content'] ?>
</h1>


----------------------------------------------------------------------------------------
---------------------------------  Bootstrap - font end framework ---------------
----------------------------------------------------------------------------------------
 Bootstrap is a free and open-source collection of tools for creating websites and web applications. It contains HTML- and CSS-based  design templates for typography, forms, buttons, navigation and other interface components, as well as optional JavaScript extensions.
It aims to ease the development of dynamic websites and web applications.

Bootstrap is a front end framework, that is, an interface for the user, unlike the server-side code which resides on the "back end" or server.

 I am inspired by this Laravel intro tutorial about I talked in a previous post: http://learninglaravel.net/laravel5/building-our-first-website

 Go to:  http://getbootstrap.com/getting-started

Click Download Bootstrap to download latest compiled Bootstrap files.

Uncompress the downloaded .zip file. We have three folders:

    css
    js
    fonts

Put them all into the public folder of your app
In the <head> of the main template file the following should be added in order to use Bootstrap framework features:

    <link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" >
    <link rel="stylesheet" href="/css/bootstrap-theme.min.css">

    <script src="/js/jquery-1.11.3.min.js"></script>
    <script src="/js/bootstrap.min.js"></script>


Under /Views create a file called "master.template.php" as you can guess this will be the template used by the entire website.
Each view will extend this master template. The master.template.php is containing navigation bar with several buttons and php code to include sub-templates same as we had inside "template.php":

<?php
    // The _content slot is a special slot set by the PhpEngine. It contains the content of the subtemplate.
    $view['slots']->output('_content');
?>


Download the entire code source and review these files.

each view will have a name like "name_of_the_view.view.php"
for example new home.view.php:

<?php $view->extend('master.template.php') ?>

<?php $view['slots']->set('title', $page['title']) ?>

    <div class="container">
        <div class="row banner">

            <div class="col-md-12">

                <h1 class="text-center margin-top-100 editContent">
                 
                </h1>
               
                <h3 class="text-center margin-top-100 editContent"><?php echo $page['content'] ?></h3>

               

            </div>

        </div>
    </div>


-----------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------
-----------------------------------------  models  ---------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------
Create DB from phpMyAdmin
framework

user  framework
pass  1234
-----------
created a table called "test" with fields: id, user, pass
add some rows manually

-------
add in index.php a path to Pages:Users     

$app->map('/users','CPANA\Controllers\Pages::Users');

add the method Users in the Pages class

public static function Users()
    {   
        $model=new UsersModel();
        $msg=$model->getUsers();
        .....
        $content=$templating->render('users.view.php', array('page' => $msg,'title' => 'Users'));

----------------------
In here we create a new instance of the model (we will create UsersModel class next).
Use the method getUsers() from Model to retrieve the users.

Send to View the array containing data from DB ('page'=>$msg) and add a new variable 'title' to store the title.
-----------------------------------------
Let's have a look at at our first model under CPANA\Models\UserModel
In constructor we initialize the connection using PDO. This should be moved to an external class to handle the connection, maybe static one.
use \PDO;

class UsersModel
{   
    public  $db;
    public function __construct()
    {   
        $user='framework';
        $pass='1234';
        $this->db = new PDO('mysql:host=localhost;dbname=framework', $user, $pass);
        $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    }
   
    public function getUsers()
    {   
        try {
            $stmt = $this->db->query('SELECT * FROM test');
            $results = $stmt->fetchAll(PDO::FETCH_ASSOC);   
        } catch(PDOException $ex) {
            $results="There was an error when connecting to the database";
        }
        return $results;       
    }


This was a good exercise for getting to know individual components from Symfony framework. In order to obtain a working website it should be further developed by adding forms, validation and security components.

The code is available at this link: https://drive.google.com/file/d/0B4lszAGYHn-dMFJqN0lwYUdWeDg/view?usp=sharing
  

No comments:

Post a Comment