Thursday, September 24, 2015

CPANABasicBlogBundle for Symfony2

Having as a starter point the http://tutorial.symblog.co.uk/ ( about which I wrote here ) I developed blog bundle having the following main features:

Frontside
- view all blog posts with pagination
- view individual blog posts
- add comments

Admin
- view a list of all blog posts with Edit and Delete options.
- view the list of comments with options to Approve/Unapprove or Delete.

Adding comments form


In the Symblog tutorial there was a add comment functionality but for me didn't worked throwing an error like "Catchable Fatal Error: Object of class XYZ could not be converted to string in Doctrine\DBAL\Statement.php" ( see this post about it ). To solve the issue I've added to Entity\Blog.php the following function:

    public function __toString()
    {
        return strval($this->id);
    }

Also I modified the CommentType.php to make all the comments unapproved initially:

// \src\CPANA\BasicBlogBundle\Form\CommentType.php

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('user')
            ->add('comment')
            ->add('approved','hidden', array('data' => '0',))
         ;
    }

Uploading photos

What I want to implement: upload photo, copy the file in a specific path  and save the name of the file on the database

Make sure you are pointing to the right path in the template. In src\CPANA\BasicBlogBundle\Resources\views\Blog\show.html.twig:

<img src="{{ asset(['bundles/basicblogbundle/images/', blog.image]|join) }}" alt="{{ blog.title }} image not found"  />

At this path: 'bundles/basicblogbundle/images/' I should upload the files in the controller.

Make sure to indicate that the "file" field is not mapped to the Entity.

->add('image', 'file', array('mapped'=>false))

Upload photo and save in database only the name
------------------------------------------------
if ($form->isValid()) {
            $newFilename = $form['attachment']->getData()->getClientOriginalName();
            // Handle picture upload process
            $uploadDir=dirname($this->container->getParameter('kernel.root_dir')) . '/web/bundles/basicblogbundle/images/';
            $form['image']->getData()->move($uploadDir,$newFilename);
            // End of upload
           
            $blog->setImage($newFilename);
            $em = $this->getDoctrine()->getManager();
            $em->persist($blog);
            $em->flush();


This works fine but we should make sure the file names are unique so they do not conflict when you try to upload a file with same name.

We should add random string function in a class. I do no think this should be considered a service, so I will not register it as a service in the Dependency Injection container.
http://symfony.com/doc/current/best_practices/business-logic.html

Create an Utils folder. Place there RandomString.php, add a static function: randomStr

namespace CPANA\BasicBlogBundle\Utils;

class RandomString
{

    public static function randomStr($length = 10)
    {
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $charactersLength = strlen($characters);
        $randomString = '';
        for ($i = 0; $i < $length; $i++) {
            $randomString .= $characters[rand(0, $charactersLength - 1)];
        }
        return $randomString;
   
    }



After this include the randomStr in the fileName:

$newFilename =RandomString::randomStr() . $form['image']->getData()->getClientOriginalName();
---------------------------
when testing the new feature I noticed that the photo is not available when browsing the blog, reason: the name included in the template is trimmed. I looked in the database , in the "image" column, same there. Checking the type of column I noticed the column is set to     varchar(20)!!!! that's the problem right there.

Solution:
modify in Entity\Blog.php the ORM annotation for $image from
@ORM\Column(type="string", length=20)
to
@ORM\Column(type="string", length=255)

After this update the DB schema using Symfony 2 command line:

php app/console cache:clear
php app/console doctrine:schema:update --force

 Pagination

In the home view of the blog I should list all the blog posts. I want to limit to 3 per page an have buttons to navigate though posts.

The path should have a parameter "currentPage" which can be optional, and the default value is 1:

CPANABasicBlogBundle_homepage:
    pattern:  /blog/{currentPage}
    defaults: { _controller: CPANABasicBlogBundle:Blog:blogHome, currentPage: 1 }
    requirements:
        _method:  GET
        currentPage: \d+


--------------
Modify the repository as seen here - http://anil.io/post/41/symfony-2-and-doctrine-pagination-with-twig:

        public function getAllPosts($currentPage = 1)
        {
            // Create our query
            $query = $this->createQueryBuilder('p')
                ->orderBy('p.created', 'DESC')
                ->getQuery();

            $paginator = $this->paginate($query, $currentPage);

            return $paginator;
        }


        public function paginate($dql, $page = 1, $limit = 3)
        {
            $paginator = new Paginator($dql);
           
            $paginator->getQuery()
                ->setFirstResult($limit * ($page - 1)) // Offset
                ->setMaxResults($limit); // Limit

            return $paginator;
        }

----------------------------------------------------
In controller retrieve posts and pass them to view

    public function blogHomeAction($currentPage=1)
    {
        $em = $this->getDoctrine()
            ->getEntityManager();

        $posts = $em->getRepository('CPANABasicBlogBundle:Blog')
            ->getAllPosts($currentPage);
       
        $iterator=$posts->getIterator();
        $limit = 3;
        $maxPages = ceil($posts->count()/$limit);
        $thisPage = $currentPage;
       
        return $this->render(
            'CPANABasicBlogBundle:Blog:home.html.twig', array(
            'blogs' => $iterator,
            'maxPages'=>$maxPages,
            'thisPage' => $thisPage,
            )
        );


In template showing the posts is the same we just need to add pagination buttons:
This will look nice with some css :)

{% if maxPages > 1 %}
    <ul>
        {%if thisPage > 1 %}
        <li >
                <a href="{{ path('CPANABasicBlogBundle_homepage', {currentPage: thisPage-1 < 1 ? 1 : thisPage-1}) }}">«</a>
        </li>
        {% endif %}
       
        {# Render each page number #}
        {% for i in 1..maxPages %}
        <li>
            <a href="{{ path('CPANABasicBlogBundle_homepage', {currentPage: i}) }}">{{ i }}</a>
        </li>
        {% endfor %}

        {# `»` arrow #}
        {%if thisPage < maxPages %}
        <li>
            <a href="{{ path('CPANABasicBlogBundle_homepage', {currentPage: thisPage+1 <= maxPages ? thisPage+1 : thisPage}) }}">»</a>
        </li>
        {% endif %}
    </ul>
    {% endif %}


The photo should not be a mandatory information. I had to do the following modifications to implement that:

Add in controller to the form builder 'required' => false :
 ->add('image', 'file', array('mapped'=>false,'required' => false,))

Also in controller handle new uploaded file name and path only if it was any file selected in the form:

if (!is_null($form['image']->getData())) {....}
 

modify the Entity/Blog.php to accept NULL values for image field.

 * @ORM\Column(type="string", length=255, nullable=true)
Update database using console command.
        php app/console cache:clear
        php app/console doctrine:schema:update --force

Fine tuning - allow deleting blog posts even if they have comments

While browsing happily and testing the functionality I discovered I cannot delete an article having comments, receiving some nasty error: "SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails ..."

I believe it should be good that an admin to be able to delete an article even if it has comments added to it. So I will modify the Database structure to allow this behavior.

There are two kinds of cascades in Doctrine:

1) ORM level - uses cascade={"remove"} in the association - this is a calculation that is done in the UnitOfWork and does not affect the database structure. When you remove an object, the UnitOfWork will iterate over all objects in the association and remove them.

2) Database level - uses onDelete="CASCADE" on the association's joinColumn - this will add On Delete Cascade to the foreign key column in the database:

@ORM\JoinColumn(name="father_id", referencedColumnName="id", onDelete="CASCADE")


Update the Entity/Comment.php
    /**
     * @ORM\ManyToOne(targetEntity="Blog", inversedBy="comments")
     * @ORM\JoinColumn(name="blog_id", referencedColumnName="id",  onDelete="CASCADE")
     */
    protected $blog;


Update the database using console:
    php app/console cache:clear
    php app/console doctrine:schema:update --force


I will soon add the BasicBlogBundle on Github and Packagist.

Thursday, September 10, 2015

Symfony 2 Authentication and FOSUserBundle


It was easier than I expected, but there are some small pieces of information  missing in the documentation which can make you lose time on StackOverflow :P.


I have a fresh installation of Symfony Framework 2.8 DEV Standard Edition. I've added Bootstap to my template by directly downloading the files locally and including them in base template.

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, should be called "web"
In the <head> of the main template (base.html.twig found under "..\symfony\app\Resources\views\base.twig.html")  the following should be added in order to use Bootstrap framework features:

        {% block stylesheets %}
            <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
            <!-- Include roboto.css to use the Roboto web font, material.css to include the theme and ripples.css to style the ripple effect -->
            <link href="{{ asset('/css/roboto.min.css') }}" rel="stylesheet">
            <link href="{{ asset('/css/material.min.css') }}" rel="stylesheet">
            <link href="{{ asset('/css/ripples.min.css') }}"  rel="stylesheet">
        {% endblock %}


Also before </body> it should be added a javascript Twig block:

        {% block javascripts %}
            <script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
            <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>

            <script src="{{ asset('/js/ripples.min.js') }}"></script>
            <script src="{{ asset('/js/material.min.js') }}"></script>
            <script>
                $(document).ready(function() {
                    // This command is used to initialize some elements and make them work properly
                    $.material.init();
                });
            </script>   
        {% endblock %}
    </body>



Inside the <body> tag I added a navigation bar ( I've seen this on this tutorial : http://laravel.com/docs/5.1/installation#basic-configuration):

<!-- Navigation bar at the top of all pages    -->
        <nav class="navbar navbar-default">
            <div class="container-fluid">
                <!-- Brand and toggle get grouped for better mobile display -->
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="/"> Symfony 2.8 website</a>
                  
                </div>

                <!-- Navbar Right -->
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <ul class="nav navbar-nav navbar-right">
                        <li class="active"><a href="/">Home</a></li>
                        <li><a href="/about">About</a></li>
                        <li><a href="/users">Users</a></li>
                        <li><a href="/contact">Contact</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Member <span class="caret"></span></a>
                            <ul class="dropdown-menu" role="menu">
                                     <li><a href="/register">Register</a></li>
                                    <li><a href="/login">Login</a></li>
                            </ul>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>


---------------------------------------------------------------------------------------------------------------------------
-------------------------------------------   Getting Started With FOSUserBundle  ------------------
----------------------------------------------------------------------------------------------------------------------------
Documentation:
https://symfony.com/doc/master/bundles/FOSUserBundle/index.html

Followed the 7 steps described in documentation. You should do those in parallel with the below notes.

    Download FOSUserBundle using composer
    Enable the Bundle
    Create your User class
    Configure your application's security.yml
    Configure the FOSUserBundle
    Import FOSUserBundle routing
    Update your database schema

-------------------------------------------------------------------------------------------------------------------------
http://stackoverflow.com/questions/19677807/symfony-understanding-super-admin
-------------------------------------------------------------------------------------------------------------------------
Make sure your security.yml contains:

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: bcrypt

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    firewalls:
        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: security.csrf.token_manager
            logout:       true
            anonymous:    true

    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/, role: ROLE_ADMIN }
        - { path: ^/user/, role: ROLE_USER }

       

-------------------------------------------------------------------------------------------------
http://symfony.com/doc/current/bundles/FOSUserBundle/command_line_tools.html

Create a super-admin user from command line:

php app/console fos:user:create admin admin@test.com 1234 --super-admin

Create a normal user:

php app/console fos:user:create regularuser regular@test.com 1234

Make an UserController which will have a path for /admin/{name}  and /user/{name}

    /**
     * @Route("/user/{name}")
     */
    public function indexAction($name)
    {
         return $this->render('AppBundle:User:userhome.html.twig',array('name' => $name));
    }
    /**
     * @Route("/admin/{name}")
     * @Template()
     */
    public function indexAdminAction($name)
    {
        return $this->render('AppBundle:User:adminhome.html.twig');
    }
   

And also you should add templates for each of them at "symfony\src\AppBundle\Resources\views\User\"
Now you can put in your browser http://<your_virtual_host>/user/YourName
You should be redirected to a login page. Login with regularuser and you will be redirected to home page
----------------------------------------------------------------------------------------------------------------------------
--------------------------------   Override templates  ------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------
Documentation:
https://symfony.com/doc/master/bundles/FOSUserBundle/overriding_templates.html

To override the layout template located at Resources/views/layout.html.twig in the FOSUserBundle directory,  you would place your new layout template at app/Resources/FOSUserBundle/views/layout.html.twig.

As you can see the pattern for overriding templates in this way is to create a folder with the name of the bundle class in the app/Resources directory. Then add your new template to this folder, preserving the directory structure from the original bundle.

----------------------------------------------------------------------------------------------------------------------------
------   Adding new fields to the User object/ Overriding Default FOSUserBundle Forms  -----------
----------------------------------------------------------------------------------------------------------------------------
https://symfony.com/doc/master/bundles/FOSUserBundle/overriding_forms.html
Following the instructions in the documentation I created the file \src\AppBundle\Entity\User.php
Make sure to add the below line in Entity\User.php

    use Symfony\Component\Validator\Constraints as Assert;

I added two new properties:

   /**
     * @ORM\Column(type="string", length=255)
     *
     * @Assert\NotBlank(message="Please enter your name.", groups={"Registration", "Profile"})
     * @Assert\Length(
     *     min=3,
     *     max=255,
     *     minMessage="The name is too short.",
     *     maxMessage="The name is too long.",
     *     groups={"Registration", "Profile"}
     * )
     */

    protected $name;
    /**
     * @ORM\Column(type="string", length=255)
     *
     * @Assert\NotBlank(message="Please enter your last name.", groups={"Registration", "Profile"})
     * @Assert\Length(
     *     min=3,
     *     max=255,
     *     minMessage="The name is too short.",
     *     maxMessage="The name is too long.",
     *     groups={"Registration", "Profile"}
     * )
     */

    protected $last_name;


Use command line to automatically generate getters and setters for your new fields:

    php app/console doctrine:generate:entities AppBundle

   
After this update the DB using Doctrine command line:

    php app/console doctrine:schema:update --force

Make sure to add the new service in services.yml:

    services:
    #    service_name:
    #        class: AppBundle\Directory\ClassName
    #        arguments: ["@another_service_name", "plain_value", "%parameter_name%"]
        app.form.registration:
            class: AppBundle\Form\RegistrationType
            tags:
                - { name: form.type, alias: app_user_registration }


Now you should be able to register a new user
----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------  Handling the log out --------------------------------------------
----------------------------------------------------------------------------------------------------------------------------

Modify the base.html.twig template which contains buttons for login and register. Verify if the user is logged and if it is, display only Logout button:

                                {% if app.user %}
                                    # user is logged in
                                    <li><a href="/logout">Logout</a></li>
                                {% else %}
                                    # user is not logged in
                                    <li><a href="/register">Register</a></li>
                                    <li><a href="/login">Login</a></li>
                                {% endif %}

----------------------------------------------------------------------------------------------------------------------------
-------------------- Show hello "Name Last name" of the logged user ------------------------------------
----------------------------------------------------------------------------------------------------------------------------
After login the user is redirected to the home page. Let's modify the template in order to show
Hello "Name Last name"!

                    {% if app.user %}
                       
                        <h3 class="text-center margin-top-100 editContent">
                            Home page. Welcome {{ app.user.name }} {{ app.user.lastname }} !
                        </h3>
                    {% else %}
                       
                        <h3 class="text-center margin-top-100 editContent">Home page. Welcome! </h3>       
                    {% endif %}

"name" and "lastname" are two properties added by me to the AppBundle\Entity\User.php class. In fact I added "last_name" in User.php  but Doctrine made a setter and getter for "lastname": getLastName.

-----------------------------------------------------------------------------------------------------------------------------
------------------------------ FOSUserBundle form labels  --------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------
You could see some weird labels in the login and registation forms. You can make them all disappear by turning on Internationalization (translator service).
This is very simple, have a look on the official documentation: http://symfony.com/doc/current/best_practices/i18n.html

Uncomment the following translator configuration option and set your application locale

# app/config/config.yml
framework:
    # ...
    translator: { fallbacks: ["%locale%"] }

# app/config/parameters.yml
parameters:
    # ...
    locale:     en

After this you can check the Login page to see the results.
---------------------------------------------------------------------------------------------------------------------
How to overwrite fos user bundle form labels?

You can read the answer on StackOverflow: http://stackoverflow.com/questions/13473329/how-to-overwrite-fos-user-bundle-form-labels  or here:   "Copy/paste vendor/friendsofsymfony/user-bundle/FOS/UserBundle/Resources/translations/FOSUserBundle.xx.yml files in your  app/Resources/FOSUserBundle/translations directory (with the same directory structure and the same file  name) and redefine translations to your convenience."

Tuesday, September 8, 2015

Build a blog bundle with Symfony 2 - Symblog tutorial errors/deprecated functions

Symblog tutorial ( http://tutorial.symblog.co.uk/ ) is one of the best I found about developing and testing a Symfony 2 bundle.
But is not all perfect, some of the function used in the tutorial are deprecated and will not work.
I found another blog were some of the issues are solved:
 http://donna-oberes.blogspot.com/2013/09/symfony-2-symblog-tutorial-errors.html

Please see below a list comments that you help you finish the tutorial successfully.
I hope this blog post will help some fellows lost  in the path of learning Symfony 2.

1. In controller repalce "bindRequest()" deprecated function with "bind()"

public function contactAction()
    {
        $enquiry = new Enquiry();
        $form = $this->createForm(new EnquiryType(), $enquiry);
       
        $request = $this->getRequest();
        if ($request->getMethod() == 'POST') {
            $form->bind($request);


2. I stoled this from a comment on the Symblog website:

FYI for symfony2.3 (and 2.4) users:

{% if app.session.hasFlash('blogger-notice') %}
<div class="blogger-notice">
{{ app.session.flash('blogger-notice') }}
</div>
{% endif %}


needs to be changed to

{% for flashMessage in app.session.flashbag.get('blogger-notice') %}
<div class="blogger-notice">
{{ flashMessage }}
</div>
{% endfor %}


Also you need to change the “setFlash” in PageController to call  “getFlashBag()->add”


3. Doctrine basic usage in official documentation:

http://symfony.com/doc/current/book/doctrine.html

4. In  BlogController:

DEPRECATED - getEntityManager is deprecated since Symfony 2.1. Use getManager instead

Change path to images in show.htmltwig. it should point to specific bundle folder in the public "web" folder:

 <img src="{{ asset(['bundles/bloggerblog/images/', blog.image]|join) }}


5. Doctrine migrations

http://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html

$ composer require doctrine/doctrine-migrations-bundle "^1.0"

In AppKernel.php you need to change this

new Symfony\Bundle\DoctrineMigrationsBundle\DoctrineMigrationsBundle() //old path

with this

new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle()


----------------------------------------------------------------------------------------------------------
Some errors messages that may make you land on this page:

FatalErrorException: Error: Call to undefined method Symfony\Component\Form\Form::bindRequest() in /path/to/root/src/Blogger/BlogBundle/Controller/PageController.php 
FatalErrorException: Error: Class 'Symfony\Component\Validator\Constraints\MaxLength' not found in /path/to/root/src/Blogger/BlogBundle/Entity/Enquiry.php




Sunday, September 6, 2015

PHP login to another website with Goutte

I needed a break from all these frameworks and design patterns and code something which would solve me some immediate problem => bring immediate satisfaction.

Task: login to a website, access certain page, check if that page was updated, let me know via email.

After searching the web a little I found Goutte (https://github.com/FriendsOfPHP/Goutte), another library from Fabien Potencier.

My composer.json looks like this:

{
    "require": {
        "fabpot/goutte": "^3.1",
        "swiftmailer/swiftmailer": "@stable"
    }
}

1. Login to website and access certain page:


require_once __DIR__.'/vendor/autoload.php';
require_once 'private.php';  //I store users and passwords in an external file

use Goutte\Client;


// make a real request to an external site
$client = new Client();
$crawler = $client->request('GET', "http://www.testwebsite.com/login");

// select the form and fill in some values
$form = $crawler->selectButton('Login')->form();
$form['login-username'] = $odds_user;
$form['login-password'] = $odds_pass;

// submit that form
$crawler = $client->submit($form);
$crawler = $client->request('GET', "http://www.testwebsite.com/somepage");

//extract data 


$table_main = $crawler->filterXPath('//table[@class="some-class"]');

2. Check if page was updated and notify by email

2.1 First  configure SwiftMailer to work with your gmail account:

//---------------------------- Email configuration  -----------------------------------------
// Create the Transport
$transporter = Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, 'ssl')
  ->setUsername($email_user)
  ->setPassword($email_pass);


// Create the Mailer using your created Transport
$mailer = Swift_Mailer::newInstance($transporter);


2.2. Check if the size of the table filtered is the same as the previous size saved on disk in the file "previous_size.txt". If not then send email


$mail_content=$table_main->text();
$size=strlen($mail_content);

$old_value = file_get_contents('previous_size.txt');


//replace old value with new size and send me email
if ($old_value!=$size) {

    file_put_contents('previous_size.txt',$size);
   
    // Create the message
    $message = Swift_Message::newInstance()

          // Give the message a subject
          ->setSubject('New bet')

          // Set the From address with an associative array
          ->setFrom(array('youremail@gmail.com' => 'Script'))

          // Set the To addresses with an associative array
          ->setTo(array('personalemail@yahoo.com' => 'CPana'))

          // Give it a body
          ->setBody($mail_content)

          // And optionally an alternative body
          ->addPart('<q>' . $mail_content .'</q>', 'text/html')

          ;
   
    // Send the message
    $result = $mailer->send($message);
}


 

 



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
  

Tuesday, August 18, 2015

Pimple dependency injector container. Lambdas and Closures

As you probably saw in a previous post a give it a try on Laravel and I felt is too high level at this moment and I should learn more about the magic behind scene not just how to use a certain framework.
So I looked for a microframework hoping to be easier to understand and I discovered Silex  from the guy who made Symfony: "Silex is a PHP microframework for PHP. It is built on the shoulders of Symfony2 and Pimple and also inspired by sinatra." Next question what is Pimple?
Answer: Pimple is a small Dependency Injection Container for PHP. ( http://pimple.sensiolabs.org/ )

I have no idea about Dependency Injectors, but  found some good materials about Dependency Injection and Pimple here:

http://www.sitepoint.com/dependency-injection-with-pimple

http://fabien.potencier.org/do-you-need-a-dependency-injection-container.html

http://www.slideshare.net/fabpot/dependency-injection-in-php-5354 


Some key ideas from the Fabien's slides:


A Dependency Injector Container/ Service Container manages your SERVICES
Good rule of thumb: It manages "global" objects (objects with only one instance !=Singleton)
Example of such objects: a User, a Request, a database Connetcion, a Logger
Unlike Model objects (a Product, a blog Post)

Dependency Injection Container is simply a PHP object that manages instantiation of other objects. we call them services.


Before starting to read these articles I have to tell you that the actual implementation is based on lambdas and closures. You should have a look before on the following links to get the idea behind this PHP features:

http://culttt.com/2013/03/25/what-are-php-lambdas-and-closures/


About closure binding here:  https://www.christophh.net/2011/10/26/closure-object-binding-in-php-54/
From this article http://programmingarehard.com/2013/10/16/we-need-some-closure.html/ :

"Nowadays, it's very common to see Closures being used for containers. These containers' purpose is to create objects and inject their dependencies so we don't have to do that every single time. Consider the following example.

$container = [];

$container['EmailTemplate'] = function(){

    return new EmailTemplate;
};

$container['DataSource'] = function(){

    return new MySQLDataSource;
};

$container['NewsLetterSender'] = function() use ($container) {

    //used to created emails
    $template   = $container['EmailTemplate']();

    //used to track stats about the sending
    $dataSource = $container['DataSource']();

    return new NewsLetterSender($dataSource, $template);
};

$newsLetterSender = $container['NewsLetterSender']();

//versus the more verbose and less flexible

$newsLetterSender = new NewsLetterSender(new EmailTemplate, new MySQLDataSource);
 
As you can see, the $container's only responsibility is to create and assemble new objects. This concept is known as Inversion of Control. It's so popular because it's an elegant way to encourage dependency injection."