Sunday, March 20, 2016

CPANAHierarchyBundle - Symfony and Neo4j

I've just uploaded to Github my last project: HierarchyBundle


This Symfony bundle manages a company hierarchy retrieving and stroring information to a Neo4j database. The hierarchy is based on groups: PHP group is part of Software Developement group, which is part of IT, etc. The users are members of these groups having different kind of roles (manager, employee etc).

I've got the idea for this bundle from the Neo4j documentation: http://neo4j.com/docs/stable/examples-user-roles-in-graphs.html . Basically it says that relational database are not really suited for representing hierarchical data :)

HierarchyBundle is using the Neo4jPHP library https://github.com/jadell/neo4jphp . On top of this I've created a class called GroupHierarchyManagement which contains specific functions for this scope, like retrieving the complete hierarchy above a user:



    /**
  * Return the hierarchy of of groups above one user node
  *
  * @param  Everyman\Neo4j\Node   node
  * @return row| null
  */
    public function getGroupHierarchyOfUser($node)
    {
        $id = $node->getId();

        $queryString =  ' MATCH n-[rel:'. $this->defRelTypeUserToGroup .']->group '.
                        ' WHERE id(n)='.$id.' '.
                        ' WITH group '.
                        ' MATCH p=group-[r:PART_OF*..]->d '.
                        ' WHERE ID(d)='.$this->rootGroupId.' '.
                        ' RETURN nodes(p) as nodes';

        $query = new Query($this->client, $queryString);
        $result = $query->getResultSet();

        if ($result->count() != 0) {
            return $result->current()->current();
        } else {
            return;
        }
    }


This class is configured as a service with the Symfony Dependency Injection Container. There are several parameters to be configured in order to work correctly:


#app/config/config.yml

cpana_hierarchy:
    group_hierarchy_manager_neo4j:
        neo4j_user:  'neo4j'
        neo4j_password:  'parola'
        def_rel_type_group_to_group: 'PART_OF'
        def_rel_type_user_to_group: 'MEMBER_OF'
        root_group_id: '69374'
        manager_role_property: 'manager'
        default_property_group:  'name'
        default_property_user: 'name'


Th first two are the user and password in order to login to Neo4j database. Next are the types (names) of the relations between Groups and Groups or between Users and Groups.
We can say that the team "PHP " is "PART_OF" departmenet "Software developement".
Also we can say employee Kenny is "MEMBER_OF" team "PHP".

You need to specify which is the root group node of your hierarchy by prodiving the Neo4j Id of that node in parameter "root_group_id".

The relation "MEMBER_OF" between Users and Groups has a property called "role", you can define which is the value of this property for users which are managers of a certain group: it can be a number, or explicitly  "manager", or "master" :) etc.

A Group or a User node can have many properties, like name, description for Groups, or age, salary for Users, from these we need to configure which to be displayed by default when we talk about that node, the name should be the most obvious value.

With this service called " group_hierarchy_manager_neo4j" I've built an application with a front side where regular users can browse the data and an Admin area to edit  the data.
Below some photos to get the idea:
Front side:

  

Admin side :


Tuesday, March 15, 2016

Symfony: How to embed a Collection of Forms and customize the form field Prototype

This article is based on the following doc articles:

http://symfony.com/doc/current/cookbook/form/form_collections.html
http://symfony.com/doc/2.7/reference/forms/types/collection.html
http://toni.uebernickel.info/2012/03/15/an-example-of-symfony2-collectiontype-form-field-prototype.html

In my example below I will be using Doctrine's  One-To-Many Bidirectional relation. Let's say we have a Feedback form to which you can add none or many Actions to be taken. Feedback form contains information like name and email of the person and Action form contains information like action description, deadline date, importance level. I will eliminate any details which are not relevant to the subject (like all the mappings, validations etc)/

Entities:

Feedback
<?php

class Feedback
{
    
    /**
     * @ORM\OneToMany(targetEntity="\MyBundle\Entity\Action", mappedBy="fkFeedback", cascade={"persist","remove"})
     */
    private $actions;
    
    private $name;
    
    private $email;


    public function __construct() {
        $this->actions = new ArrayCollection();
    }

    public function getActions()
    {
        return $this->actions;
    }

    public function addAction(\MyBundle\Entity\Action $action) 
    {
        $action->setFkIdFeedback($this);
        $this->actions->add($action);
        return $this;
    }
 
    public function removeAction($action) 
    {
        if ($this->actions>contains($action)) {
            $this->actions->removeElement($action);
        }
        return $this;
    }

}

Action

<?php

class Action

{
    private $actionDesc;

    private $importance;

    private $deadlineDate;

    /**
     * @ORM\ManyToOne(targetEntity="Feedback", inversedBy="actions")
     * @ORM\JoinColumn(name="fk_feedback", referencedColumnName="id", nullable=FALSE )
     */
    private $fkFeedback;
}


Note mappedBy and inversedBy properties. Also on the One side I have added information regarding how Doctrine should persist the associated entities: cascade={'persist'}.
Also 'addAction()' function makes sure the changes are reflected in both sides of the relation.

 Forms


<?php

class FeedbackType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name','text',array('label' => false, 'required' => false, 'attr'=> array('placeholder'=> 'Last name')))
            ->add('email','email',array('label' => false, 'required' => false, 'attr'=> array('placeholder'=> 'Email')))
            ->add('actions', 'collection', array(
                'type' => new ActionType(),
                'allow_add'    => true,
                'allow_delete' => true,
                'by_reference' => false,
                'cascade_validation' => true,
                'error_bubbling' => false,
            ));
    }
    
    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'MyBundle\Entity\Feedback',
            'cascade_validation' => true,   
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'mybundle_feedback';
    }
}

On the FeedbackType class (One side from the One-To-Many relation) I set a collection of ActionType forms. Also 'error_bubbling' to false because I want to get errors from Action form under each field and not only at the form level: form_error(form) .

The ActionType class has nothig special in it, I will not listed here.

Allowing the user to dynamically add new Actions means that you'll need to use some JavaScript.
The option 'allow_add' is needed to this flexibility to be possible.

In addition to telling the field to accept any number of submitted objects, the allow_add also makes a "prototype" variable available to you. This "prototype" is a little "template" that contains all the HTML to be able to render any new "tag" forms.

If you render the entire form you will see in a HTML  data-*  attribute the prototype for creating new Actions. From here on I will not follow the example found in official documentation, instead I will render the form and use jQuery code like in this blog article.

Rendering the form

The prototype macro

In the situation when by default I want to render 2 actions to be filled and allow dynamically to be added other there may be differences between how the widget is rendered and how the prototype is rendered. The below Twig macro handles this issue.
This macro renders the prototype and the actual widget the same way. Therefore the resulting usage code is very little and required javascript code just works for all collections.


{% macro widget_prototype(widget, remove_text) %}
        {% if widget.vars.prototype is defined %}
            {% set form = widget.vars.prototype %}
            {% set name = widget.vars.prototype.vars.name %}
        {% else %}
            {% set form = widget %}
            {% set name = widget.vars.full_name %}
    {% endif %}

        <div data-content="{{ name }}"  class="panel panel-default">
            <div class="panel-body">
                {{ form_row(form.actionDesc) }}
                {{ form_row(form.importance) }}
                {{ form_row(form.deadlineDate, {'attr':{'class':'actions-deadline'}}) }}
                <a class="btn btn-danger" data-related="{{ name }}">{{ remove_text }}</a>
            </div>
            
            
        </div>
    {% endmacro %}

As an example I customized the class attribute for deadlineData to be able to identify them for adding  Datepicker jQuery UI plugin.  The values  widget.vars.full_name and widget.vars.name are related to form object not to my actual Entity fields (name, email).

The actual form rendering:

I am not rendering the form using form_start, form_end in order to get more control on what is displayed. If you are using Bootstrap you can use one of the Twig form layouts offered by Symfony team:


        {% form_theme form 'MyBundle:Form:bootstrap_3_layout.html.twig' %}
        <div class="row">
            <div class="col-sm-3"></div>
            <div class="col-sm-6">
                <h3 style="color:white;">Feedback</h3>
                <form name="docsbundle_feedback" method="POST">
                        {{ form_errors(form) }}
                    <fieldset>           
                        {{ form_row(form.name) }}
                        {{ form_row(form.email) }}
                        <hr>
                         <h3 style="color:white">Actions</h3>
                            <div>
                                <div id="actions" data-prototype="{{ _self.widget_prototype(form.actions, 'Remove action')|escape }}">
                                    {% for widget in form.actions.children %}
                                        {{ _self.widget_prototype(widget, 'Remove action') }}
                                    {% endfor %}
                                </div>

                                <a id="add-action" class="btn btn-info" data-target="actions">Add action</a>
                            </div>

                        <hr>               

                        {{ form_widget(form._token) }}

                        {{ form_widget(form.submit, {'attr':{'class':'btn-info'}}) }}

                    </fieldset>
                </form>
            </div>
        <div class="col-sm-3"></div>
        </div>

jQuery snippet:

 The below jQuery take care to allow adding and removing Actions.


{% block javascripts %}
    <script>
        $('#add-action').click(function(event) {
            var collectionHolder = $('#' + $(this).attr('data-target'));
            var prototype = collectionHolder.attr('data-prototype');
            var form = prototype.replace(/__name__/g, collectionHolder.children().length);

            collectionHolder.append(form);

            return false;
        });
        $('#actions').on('click','.btn-danger', function(event) {
            var name = $(this).attr('data-related');
            $('*[data-content="'+name+'"]').remove();

            return false;
        });

    </script>
{% endblock %}

Controller

In controller I am persisting only the Feedback entity, and with the option cascade={'persis'}, Doctrine knows to take care of persisting the associated Action objects


<?php

public function feedbackAction(Request $request)
    {
        
        $entity = new Feedback();
        
        $empty_action = new Action();
        $entity->addAction($empty_action);
        
        $form = $this->feedbackCreateForm($entity);
        
        $form->handleRequest($request);           
        
        if ($form->isValid()) {  
            .....
            // persist the $entity
            $em = $this->getDoctrine()->getManager();
            $em->persist($entity); 
            $em->flush();
            ...

Sunday, March 6, 2016

Neo4j intro

Neo4j is a graph database management system developed by Neo Technology, Inc. Described by its developers as an ACID-compliant transactional database with native graph storage and processing,Neo4j is the most popular graph database according to db-engines.com [wikipedia]

I am interested in Neo4j for modelling hierarchies as the RDBMS are not really suited for this ( http://www.codeproject.com/Articles/22824/A-Model-to-Represent-Directed-Acyclic-Graphs-DAG-o)

 Installing Neo4j Community Edition on Windows is a piece of cake: http://neo4j.com/download/
Also there is plenty of documentation and many answered questions on StackOverflow :)

A good place to start: https://www.airpair.com/neo4j/posts/getting-started-with-neo4j-and-cypher

Some useful queries:
--------------------------------
Match Node by id:

MATCH (n)
WHERE id(n) = 14
RETURN n;

---------------------------------
Match relationship by Id

MATCH ()-[r]-() 
WHERE ID(r)=1 
RETURN r 

------------------------------
Find path between 2 nodes, replace "REPORTS_TO" with your relation

START a=node(11), d=node(12)
MATCH p=a-[r:REPORTS_TO*..]-d
RETURN p;

----------------------------
Delete node and all its relationships

MATCH (n { name:'Andres' })
DETACH DELETE n

-----------------------------------------
DELETE all relationships

MATCH  ()-[r:REPORTS_TO]->() DELETE  r;

Importing data from CSV

Official docs: http://neo4j.com/docs/stable/query-load-csv.html

I copied the files to be imported next to the database file because I had issues with other paths. My path looks like: C:\Users\my_user\Documents\Neo4j\file_to_import.csv

Loading nodes:

LOAD CSV FROM 'file:///C:/Users/my_user/Documents/Neo4j/nodes.csv' AS line
CREATE (:User { some_id: line[0], user_name: line[1], link: line[2] })


Add an index on some_id property:

CREATE CONSTRAINT ON (n:User) ASSERT n.some_id IS UNIQUE

Load relations from CSV file, replace PART_OF with your relation:

LOAD CSV  FROM "file:///C:/Users/my_user/Documents/Neo4j/relations.csv" AS line
MATCH (p:User { some_id:line[0] })
MATCH (f:Group { group_id:line[1] })
CREATE (f)-[:PART_OF { some_property: line[2] } ]->(p);

Tuesday, February 23, 2016

Save logs to Database and access them from web UI with LexikMonologBrowserBundle

Monolog: Monolog is a logging library for PHP used by Symfony.

Cookbook:  http://symfony.com/doc/current/cookbook/logging/monolog.html

LexikMonologBrowserBundle: 
https://github.com/lexik/LexikMonologBrowserBundle




This Symfony2 bundle provides a Doctrine DBAL handler for Monolog and a web UI to display log entries.

I am using an existing Doctrine configuration in config.yml:

#config.yml

    default:
                    driver:   "%database_driver%"
                    host:     "%database_host%"
                    port:     "%database_port%"
                    dbname:   "%database_name%"
                    user:     "%database_user_prod%"
                    password: "%database_password_prod%"
                    charset:  UTF8
                    logging:   false
                    profiling: false


If you do not have the last two lines, you should add them to avoid circular reference error.
And also  add lexik_monolog_browser config to use the existing Doctrine connection:

    lexik_monolog_browser:
        doctrine:
            connection_name:  default
            table_name: monolog_entries


Generatate the table in the database:

    >php app/console lexik:monolog-browser:schema-create
   

Add the below to config_dev.yml and/or config_prod.yml in order for Monolog to use our handler to save into database.

    # app/config/config_prod.yml # or any env
    monolog:
        handlers:
            main:
                type:         fingers_crossed # or buffer
                level:        error
                handler:      lexik_monolog_browser
            app:
                type:         buffer
                action_level: info
                channels:     app
                handler:      lexik_monolog_browser
            deprecation:
                type:         buffer
                action_level: warning
                channels:     deprecation
                handler:      lexik_monolog_browser
            lexik_monolog_browser:
                type:         service
                id:           lexik_monolog_browser.handler.doctrine_dbal


Import routes to app/config/routing.yml:

    # app/config/routing.yml
    lexik_monolog_browser:
        resource: "@LexikMonologBrowserBundle/Resources/config/routing.xml"
        prefix:   /admin/monolog




More:

It would be good to save in logs also the current user. For this you can add in your controller:

       //log current user in Monolog
        $logger = $this->get('logger');
        $logger->info('Current user is : ' . $this->getUser()->getUsername());

Friday, February 5, 2016

Why and how to use services in Symfony

Controllers

Symfony follows the philosophy of “thin controllers and fat models”. This means that controllers should hold just the thin layer of glue-code needed to coordinate the different parts of the application.
As a rule of thumb, you should follow the 5-10-20 rule, where controllers should only define 5 variables or less, contain 10 actions or less and include 20 lines of code or less in each action. This isn't an exact science, but it should help you realize when code should be refactored out of the controller and into a service. Source: http://symfony.com/doc/current/best_practices/controllers.html
A unit test is a test against a single PHP class, also called a unit. If you want to test the overall behavior of your application, then use Functional Tests. Because controllers glue together the different parts of the applications they are not usually tested with Unit Tests, instead they are tested with Functional Tests.

Organizing Your Business Logic

Useful info: http://symfony.com/doc/current/best_practices/business-logic.html
It is very easy to start writing business logic into controller, but this way we will end up with a large function, difficult to read which will break separation of concerns principle https://en.wikipedia.org/wiki/Separation_of_concerns and makes it impossible to test only the business logic.
In order to better organize the code, additional classes should be created. Let's take as an example a class that handles file uploading. Basically it receives the $file uploaded in form and the path where to save the file with a unique name in order to avoid collisions with previous uploaded files. The function returns the name of the file saved on the disk to be persisted in database.
namespace YourBundle\Utils;
 
Class FileUpload
{
    public function upload(\Symfony\Component\HttpFoundation\File\UploadedFile $file, $uploadDir)
    {
            // Generate a unique name for the file before saving it
            $fileName = md5(uniqid()).$file->guessExtension();
 
            // Move the file to the directory where uploads are stored
            $file->move($uploadDir, $fileName);
 
            // Update the 'Attachement' property to store the  file name
            // instead of its contents
 
            return $fileName;
    }
}
The file uploading method is based on this article from documentation: http://symfony.com/doc/current/cookbook/controller/upload_file.html
In your controller, the FileUpload class will be used like this:

use YourBundle\Utils\FileUpload;
....
public function createAction(Request $request)
{
     ....
     if ($form->isValid()) {
            // $file stores the uploaded  file
            //  @var Symfony\Component\HttpFoundation\File\UploadedFile $file 
            $file = $product->getBrochure();
 
            // Obtain from parameters the directory where brochures are stored
            $uploadDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures';
 
            //use FileUpload class
            $uploader= new FileUpload();
            $fileName = $uploader->upload($file, $uploadDir);
 
            // Update the 'brochure' property to store the  file name
            // instead of its contents
            $product->setBrochure($fileName);
 
            // ... persist the $product variable or any other work
 
            return $this->redirect($this->generateUrl('app_product_list'));
        }
        .....
}
OK, so now we have a testable function upload() which can be tested independently from the rest of the controller, we just need a mock object of type 'Symfony\Component\HttpFoundation\File\UploadedFile' and a path.

Maintaining the code

Parameters

We are very happy with the result and we started using this class in all controllers, just that at some point we receive a request to change the path where to save the file uploaded. This means we need to search in all controllers and change this line:

$uploadDir = $this->container->getParameter('kernel.root_dir').'/../web/uploads/brochures';

In order to prevent this in future we can declare a parameter in a local parameters.yml which is imported in the global one:

parameters:
    yourbundle.upload_dir:  '%kernel.root_dir%/../web/uploads/attachments/YourBundle/Upload'

and use it in all controllers:

$uploadDir = $this->container->getParameter('yourbundle.upload_dir');

Dependencies

Now we are happy again and think nothing bad can happen to our code :). But then again there is a request to save the files to a second path, a Back Up folder let's say. We could do this by adding a third parameter in the upload() function:

public function upload(\Symfony\Component\HttpFoundation\File\UploadedFile $file, $uploadDir, $uploadDirBackUp)

And we have to modify in all controllers where we are using this function. We will declare this BackUp dir as a parameter also and use it like this:

$uploadDirBackUp = $this->container->getParameter('yourbundle.upload_dir_backup');
$fileName = $uploader->upload($file, $uploadDir, $uploadDirBackUp);

So we realize that injecting dependencies directly in our function is not a solution. Usually the recommended way to inject dependencies is in constructor definition or by using setters. Let's inject dependencies in constructor:

namespace YourBundle\Utils;
 
Class FileUpload
{
    $uploadDir;
    $uploadDirBackUp;
 
    public function __construct($uploadDir, $uploadDirBackUp)
    {
        $this->uploadDir = $uploadDir;
        $this->uploadDirBackUp = $uploadDirBackUp;
    }
    public function upload(\Symfony\Component\HttpFoundation\File\UploadedFile $file)
    {
            // Generate a unique name for the file before saving it
            $fileName = md5(uniqid()).$file->guessExtension();
 
            // Move the file to the directory where uploads are stored
            $file->move($this->uploadDir, $fileName);
            ....
    }
}
 
Even if the dependencies are injected in constructor, we would still have to modify all the controllers:

use YourBundle\Utils\FileUpload;
....
public function createAction(Request $request)
{
     ....
            // Move the file to the directory where brochures are stored
            $uploadDir = $this->container->getParameter('yourbundle.upload_dir');
            $uploadDirBackUp = $this->container->getParameter('yourbundle.upload_dir_backup');
 
            $uploader= new FileUpload($uploadDir,$uploadDirBackUp);
            $fileName = $uploader->upload($file);
        .....
}
 
The solution? Creating a service!

What is a Service?

Put simply, a Service is any PHP object that performs some sort of “global” task. It's a purposefully-generic name used in computer science to describe an object that's created for a specific purpose (e.g. delivering emails). Each service is used throughout your application whenever you need the specific functionality it provides. You don't have to do anything special to make a service: simply write a PHP class with some code that accomplishes a specific task. Congratulations, you've just created a service!
As a rule, a PHP object is a service if it is used globally in your application. A single Mailer service is used globally to send email messages whereas the many Message objects that it delivers are not services. Similarly, a Product object is not a service, but an object that persists Product objects to a database is a service.
A Service Container (or dependency injection container) is simply a PHP object that manages the instantiation of services (i.e. objects).
Source: http://symfony.com/doc/current/book/service_container.html

Create the service

In your bundle create a file called services.yml under Resources\config

services:
    yourbundle_file_upload_service:
        class: YourBundle\Utils\FileUpload
        arguments: ['%yourbundle.upload_dir%','%yourbundle.upload_dir_backup%' ]
 
Now the class will not be directly instantiated, instead we will access it from the container. In controller:

   $fileUpload = $this->get('yourbundle_file_upload_service');           
   $fileName = $fileUpload->upload($file);
 
What we've accoplished? Now each time we want to add or modify a dependency we need to edit just in two places: in the services.yml file and in the contruct() function. 

 Note: make sure that your bundle is not configured to expect a XML (services.xml) file in YourBundle\DependencyInjection\YourExtension.php:

 $loader = new Loader\YamlFileLoader($container, new FileLocator(DIR__.'/../Resources/config'));

$loader->load('services.yml');

Monday, February 1, 2016

About testing

1. Testing in Symfony

Roughly speaking, there are two types of test. Unit testing allows you to test the input and output of specific functions. Functional testing allows you to command a “browser” where you browse to pages on your site, click links, fill out forms and assert that you see certain things on the page.

1.1. Unit Tests

Unit tests are used to test your “business logic”, which should live in classes that are independent of Symfony. For that reason, Symfony doesn't really have an opinion on what tools you use for unit testing. However, the most popular tools are PhpUnit and PhpSpec.

1.2. Functional Tests

Creating really good functional tests can be tough so some developers skip these completely. Don't skip the functional tests! By defining some simple functional tests, you can quickly spot any big errors before you deploy them:

1.3. Testing JavaScript Functionality

The built-in functional testing client is great, but it can't be used to test any JavaScript behavior on your pages. If you need to test this, consider using the Mink or Selenium.

The above text is based on official Symfony documentation found here: http://symfony.com/doc/current/best_practices/tests.html

2. Acceptance testing

Acceptance testing can be performed by a non-technical person. That person can be your tester, manager or even client. If you are developing a web-application (and probably you are) the tester needs nothing more than a web browser to check that your site works correctly. You can reproduce a AcceptanceTester's actions in scenarios and run them automatically after each site change. Codeception keeps tests clean and simple, as if they were recorded from the words of AcceptanceTester.

2.1. Functional vs Acceptance testing

Acceptance tests are front-end only and you are testing whether you see the correct UI elements. This type of test never explicitly checks the database with a direct call. Think of it as only testing what an end-user would be able to see by clicking around in a browser.
Functional tests are similar to acceptance tests but they do check the database explicitly, something like $I→seeInDatabase() or $I→seeRecord(). Functional tests generally use a DOM parser instead of a browser (or phantomJS) like acceptance tests do – this is why JS is best left to acceptance tests.

3. All in one: Codeception

With Codeception you can run unit, functional and acceptance tests..
Why should I use Codeception instead of PHPUnit?
Being the most popular unit testing framework PHPUnit has very limited features for functional testing with Selenium or other backends. Codeception is PHPUnit on steroids. Everything you need for testing is built-in and works just out of the box. No more pain in configuring Selenium, data cleanup, writing XPaths, and fixtures.
Q: It looks just like Behat
Unlike Behat, Codeception tests are written in PHP. Thus, they are more flexible and easy in writing. Also you can use variables and operators, CSS and XPath locators in your tests. These features allow you to build a solid test automation platform for testing your web application. Codeception tests are simple and readable for your developers, managers, and QA team.
Q: We are planning to use Selenium IDE. Why Codeception?
Codeception works great with Selenium. But with Codeception you can write your tests in PHP. The main reason is: Selenium IDE tests are tightly bound to XPath locators. If you ever change anything in layout tests will fall. Codeception locators are more stable. You can use names, labels, button names and CSS to match elements on page. It's much easier to support the Codeception test then Selenium's one. Selenium just can't clean data between tests, can't check database values, or generate code coverage reports.
Q: Is Codeception a tool for testing legacy projects?
Sure you can use Codeception for black-box testing of your legacy application. But in the same manner you can start testing modern web application as well. With modules that integrates with all popular PHP frameworks you can start writing functional tests in seconds. Unit tests? Write them as you do in PHPUnit with some enhancement. Codeception keeps your tests in one place, makes them structured and readable.
Source: http://codeception.com/

Thursday, January 21, 2016

Using multiple databases with Symfony2 and Doctrine2

I was looking for documentation on how to use multiple databases with Symfony and all the results were talking about having 2 connections with 2 entity managers and adding mappings for each bundle ...too complicated for a simple thing.

But I kept on searching and I found this article:  https://techpunch.co.uk/development/using-multiple-databases-with-symfony2-and-doctrine2  which is saying:

"
You will need to use only connection that spans all of your databases, if you want to build a relationship between entities then they must use the same connection. Do to this you will need a user that has access to all of the databases the you wish to access. Setup this user as per usual within your Symfony2 application, for the database name just select one of the databases, it doesn't matter which one.
No extra Doctrine config is needed to get this working, ....

The key to getting multiple databases to work is within your entity classes, you need to specify the table name of the entity with a prefix of the name of the database to which the table belongs. Here is an example using annotations:

<?php
namespace Demo\UserBundle\Entity;

use DoctrineORMMapping as ORM;

/**
 * Demo\UserBundle\Entity\User
 *
 * @ORMTable(name="users.User")
 */
class User implements
{
  /* ... */

"
I tried this solution on my project and worked fine.

You may want to look also at Doctrine's  Master-Slave Connection:  http://blog.alejandrocelaya.com/2014/04/18/configure-multiple-database-connections-in-doctrine-with-zend-framework-2-2/