Sunday, December 10, 2023

Doctrine error Binding entities to query parameters only allowed for entities that have an identifier

If you have reached this page you are probably a little bit desperate to know WHY?!!! After checking doctrine mappings for a while I've solved the mystery.  In my case it was quite easy and stupid:

1. In some command I would create a new instance of MyClass() which has an autoid generated primary key called $id. 

2. At some point I would pass it to some repository function:  $this->em->searchForMyObject($myNewObject);

3. The repository function would look like:

public function  searchForMyObject(MyClass $object): ?SomeOtherClass

{

     $result = $this->createQueryBuilder('o')

            ->select('o')
->where('o.my_property = :object')
....

}


4. Reason for the error: the object was not persisted to database or retrieved from database, so it had no actual id value.

Reading the error again, now it makes sense:   "entities that have an identifier.".

Wednesday, April 28, 2021

Deploy Symfony project on AWS ElasticBeanstalk using Github Actions

 In this article I will build a simple CI/CD flow which will have the following steps:

- push code to master branch and trigger Github Action to start

- run composer install

- run any tests or static analysis tools you have, for this example I will just run PHPStan

- if everything goes OK wrap all in a zip file and deploy it to an existing AWS ElasticBeanstalk environment.

- update database as a postdeploy step

I use Github Secrets for storing AWS key access but for the other parameters from .env that I want to override I store them directly in the AWS ElasticBeanstalk > Environment > Configuration > Software.

Example of using secrets parameters in yaml config file:

  aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
  aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

In order to get started with Github Actions add to the root of your project a folder called ".github" and inside of it another one called  "workflows" and finally a file with a name for example "my_workflow.yml".

.github/workflows/my_workflow.yml

In my example the Symfony project is in a subdirectory of the repository so I have to set a working directory and also pass it as parameter for composer.

# This is a basic workflow to help you get started with Actions

name: Build and test

# Controls when the action will run. 
on:
  # Triggers the workflow on push or pull request events but only for the master branch
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:
jobs:
  php-unit-and-functional-tests:
    runs-on: ubuntu-20.04
    defaults:
      run:
        working-directory: ./html
    strategy:
      fail-fast: true
      matrix:
        php-versions: ['7.4']
    steps:
      # —— Setup Github actions —————————————————————————————————————————————
      # https://github.com/actions/checkout (official)
      - name: Git checkout placeholder-service
        uses: actions/checkout@v2
      # https://github.com/shivammathur/setup-php (community)
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-versions }}
          coverage: none
          tools: composer:v2
          extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, gd, iconv, json, mbstring, pdo
        env:
          update: true
      - name: Check PHP Version
        run: php -v
    #  --- Install backend dependencies (Composer) ----------------
      - uses: "ramsey/composer-install@v1"
        with:
          composer-options: "--ignore-platform-reqs --working-dir=html"
      # —— Symfony ——————————————————————————————————————————————————————————
      - name: Check the Symfony console
        run: bin/console -V
      # —— PHPStan --------------------------
      - name: PHPstan
        run: composer phpstan

      - name: Generate deployment package
        run: zip -r deploy.zip . -x '*.git*'

      - name: Deploy to EB
        uses: einaregilsson/beanstalk-deploy@v16
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: MyApp
          environment_name: MyApp-env
          version_label: ${{ github.sha }}
          region: us-west-2
          deployment_package: ./html/deploy.zip

After pushing a commit to master branch you can go your Github account and check the Actions tab to see the result.
In order to update database after deploy you will need to add a postdeploy step (this is a AWS ElasticBeanstalk feature). Create in your project the following folders structure:

.platform/hooks/postdeploy/01-database_update.sh 

In my case this is inside the html folder because I deploy only the content of that folder. The file 01-database_update.sh looks like this:



#!/bin/bash
#Create a copy of the environment variable file to be accessible for other CLI scripts
cp /opt/elasticbeanstalk/deployment/env /opt/elasticbeanstalk/deployment/custom_env_var

#Set permissions to the custom_env_var file so this file can be accessed by any user on the instance. You can restrict permissions as per your requirements.
chmod 644 /opt/elasticbeanstalk/deployment/custom_env_var

#Remove duplicate files upon deployment.
rm -f /opt/elasticbeanstalk/deployment/*.bak

#export env vars and update database
export $(cat /opt/elasticbeanstalk/deployment/env | xargs) && php bin/console doctrine:schema:update --force

There is an extra effort to make available to the script the environment variables (the one I setted in AWS ElasticBeanstalk > Environment > Configuration > Software) as I do not store them in git repository and they are specific for this environment.

Extra

Fine tune your ElasticBeanstalk environment using .ebextensions folder. These extensions are executed in the order of the files names so it's a good practice to call them  "01-my_extension.config" "02-my_other_action.config". Make sure to have the extension ".config".
For example you may want to set the right public directory (you can do this action also from AWS web interface). We will add a file called "01-environment.conf"


option_settings:
  "aws:elasticbeanstalk:container:php:phpini":
    document_root: /public
    memory_limit: 512M

Check AWS docs on how to tweak this here:  https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_PHP.container.html

And now you can just sit back and enjoy your state of art build and deploy flow.

Saturday, February 20, 2021

Doctrine: Given object is not an instance of the class this property was declared in

 This error confused me a little bit. I was trying to add an EntityType field to my form and I was keep getting this error. 

Executing:

php bin/console doctrine:schema:validate

Was showing that the Doctrine annotations were correctly set, but still the error was there. Trying to manually query all records from that entity in controller showed me the problem:

$companies = $em->getRepository(Company::class)->findAll();

Looking at the list of companies it was a list of objects from User class! Evrika!  I've made the repository class for Company class by copy pasting it from User class and I forgot to change the entityClass parameter in constructor:

parent::__construct($registry, Company::class);

Have a bug free day! 

Tuesday, December 15, 2020

Watch out! XDebug 3 will not work with some older PHPStorm versions

 I lost like half a day till a figure it out why XDebug from my Docker container will not work properly with PHPStorm.

In Preferences > Languages & Frameworks > PHP, add a new CLI Interpreter. Choose the Docker option, and PHPStorm will automatically find the Xdebug image for you.

At the end of this step I would see correct version of PHP, correct version of XDebug but an error message like: Xdebug: [Step Debug] Could not connect to debugging client (tried using xdebug.client_host ..).

It was driving me nuts, the debugger would stop at first line in code and when going to next breakpoint would just idle.

Short term solution: edit dockerfile to install older XDebug version:

# install xdebug
RUN pecl install xdebug-2.6.1
RUN docker-php-ext-enable xdebug

Long term solution: buy a newer PHPStorm version :) 

Tuesday, October 22, 2019

Symfony file upload mysterious error

Uncaught PHP Exception Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException: "The file "" does not exist" at /var/app/current/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php line 116 {"exception":"[object] (Symfony\\Component\\HttpFoundation\\File\\Exception\\FileNotFoundException(code: 0): The file \"\" does not exist at /var/app/current/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/File/MimeType/MimeTypeGuesser.php:116)"}


If you see this error the odds are high that you are trying to upload a file larger than the max values set in you php.ini config file (upload_max_filesize and post_max_size).

Wednesday, October 2, 2019

Symfony service container: binding arguments by name or type

This cool feature was introduced in Symfony 3.4 (https://symfony.com/blog/new-in-symfony-3-4-local-service-binding) but I didn't use till today.

Example: create a custom Monolog channel and inject the service in controller (Symfony 4).
I was thinking that I will have to create some sort of custom service definition in order to inject the service "monolog.logger.mychannel" into my controller, but no, it was way easier.

In services.yaml  just add:



services:
    # default configuration for services in *this* file
    _defaults:
        bind:
            # pass this service to any $mychannelLogger argument for any
            # service that's defined in this file
            $mychannelLogger: '@monolog.logger.mychannel'

Now is super easy to inject the service in controllers by adding "$mychannelLogger" in the list of function parameters:


/**
     * @Route("/{id}", name="project_update", methods={"POST"})
     */
    public function delete(Request $request, LoggerInterface $mychannelLogger): Response
    {
    


Read more about it in Symfony docs: https://symfony.com/doc/current/service_container.html#services-binding


Wednesday, September 18, 2019

Custom user provider using multiple database connections with Doctrine and Symfony 4

Situation :

I am using Symfony 4.3 and autowiring.

I have 2 Doctrine database connections, a default and "users" connection:



doctrine:
    dbal:
        default_connection: default
        connections:

           .....
    orm:
        auto_generate_proxy_classes: true
        default_entity_manager: default
        entity_managers:
            default:
                connection: default
                mappings:
                    Main:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/src/Entity/Main'
                        prefix: 'App\Entity\Main'
                        alias: Main
            users:
                connection: users
                mappings:
                    Users:
                        is_bundle: false
                        type: annotation
                        dir: '%kernel.project_dir%/src/Entity/UsersManagement'
                        prefix: 'App\Entity\UsersManagement'
                        alias: Users

So my User entity was managed by the secondary connection "users". Checking from command line would show me that everything is mapped correctly:

php bin/console doctrine:mapping:info --em=users

 Found 7 mapped entities:

 [OK]   App\Entity\UsersManagement\User

Also when loading users I am using a customer query by making UserRepository implement the UserLoaderInterface. (https://symfony.com/doc/current/security/user_provider.html).

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        app_user_provider:
            entity:
                class: App\Entity\UsersManagement\User

But when I try to login, ugly surprise this error pops:
 "The class 'App\Entity\UsersManagement\User' was not found in the chain configured namespaces App\Entity\Main" 

 After googling a little bit I get to this life saving comment from stof: https://github.com/symfony/symfony/pull/8187#issuecomment-18910167


Here is how you configure it:

security:
    providers:
        my_provider:
            entity:
                class: Acme\DemoBundle\Entity\User
                manager_name: users  #non default entity manager

So there is a 'secret' parameter "manager_name" that you need to add to security.yml and is not enough that the entity is correctly mapped by Doctrine.

If somebody ever finds this useful please add a comment :D

Thursday, April 20, 2017

Automated API testing using Codeception

I was looking for a tool for testing an API, and I remembered about my old friend Codeception, which I used for some functional testing a while ago. I need now to test an API which is receiving requests and gives responses using JSON format.


Install Codeception via Composer
$ composer require "codeception/codeception" 
Create an alias so you do not have to write the entire path every time you execute a command:
$ alias codecept='./vendor/bin/codecept'
Create an API test suite:
$ codecept generate:suite api
Edit the configuration file  tests/api.suite.yml:


class_name: ApiTester
modules:
    enabled:
        - \Helper\Api
        - REST:
            url: "http://yourwebste.com/"
            depends: PhpBrowser
            part: Json

I will be using "Cest" format:  "Cest combines scenario-driven test approach with OOP design. In case you want to group a few testing scenarios into one you should consider using Cest format."

$ codecept generate:cest api UsersTestCest
This command is creating a PHP file under tests/api/.  
Before starting to write my first tests I need to find a way to pass a username and password to all tests, as it is needed for connecting to the API.
I can add any parameters to the api.suite.yml and access them inside my test classes:
api.suite.yml


class_name: ApiTester
modules:
    enabled:
        - \Helper\Api
        - REST:
            url: "http://yourwebsite.com"
            depends: PhpBrowser
            part: Json
params:
    username: "username"
    password: "password"


 And inside tests/api/UsersTestCest.php

class UsersTestCest
{
    private $username;
    private $password;

    public function _before(ApiTester $I)
}
    {
        $config = \Codeception\Configuration::config();
        $apiSettings = \Codeception\Configuration::suiteSettings('api', $config);
        $this->username = $apiSettings['params']['username'];
        $this->password = $apiSettings['params']['password'];
    }
}


The function "_before()" will be executed as it names says, before actual test functions, and it will  initialize the variables with values from api.suite.yml file.

Let's write the first test function:


public function createUser(ApiTester $I)
    {
        $action = "create_user";
        $I->haveHttpHeader('Content-Type', 'application/json');

        $array=[
            "action"=> $action,
            "username"=> $this->username,
            "key"=> $this->password,
            "user_username"=> "new_username",
            "user_email"=> "email@company.com",
            "user_password"=> "password"
            ]
        ];
        $string = json_encode($array);
        $I->sendPOST('api.php',$string);
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK); // 200
        $I->seeResponseIsJson();
        $I->seeResponseMatchesJsonType([
            'operation' => 'string',
            'status' => 'string',
        ]);
    }

The link where the request is sent with the function sendPost() is the concatenation between the URL parameter from api.suite.yml and the first parameter in sentPost() function.
You can check if the response has HTTP status 200, if it is in JSON format and if the JSON has the expected structure.
Even more you can create your own assertions. These custom functions needs to be added to the file "tests/_support/ApiTester.php".
I will add a function to verify if the "status" field from the response is equal to '1'




<?php
namespace Helper;
class will be available in $I
use Codeception\TestInterface;

class Api extends \Codeception\Module
{
    public function checkOperationStatusSuccess()
    {
        $response = $this->getModule('REST')->response;
        $array = json_decode($response, true);
        $this->assertEquals('1',$array['status'],"Operation status should be '1' for success.");
    }
}

Run all tests with this command:
codecept run api -v

Or only tests from certain class:
$ codecept run tests/api/UsersTestCest.php -v
  
The tests are executed in the alphabetic order of the classes, but you can use the annotation "@depends" to indicated that your test function depends on other one(s). 
Enjoy!



Friday, October 7, 2016

Web SSO - part 2 - integration with LDAP

In my previous article I presented how to create a web SSO system using SimpleSAMLphp and Symfony. The users where declared directly in simpleSAMLphp using "exampleauth:UserPass".

In many companies a LDAP server is the source from where information about user authentication is taken. I will install  OpenLDAP and configure my applications to use it.

1. Install OpenLDAP and phpLDAPadmin

For installing OpenLDAP and phpLDAPadmin I followed this tutorials from DigitalOcean:

https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-openldap-and-phpldapadmin-on-an-ubuntu-14-04-server

You will need to edit also the ldap.conf file, see this thread on StackOverflow

Also you may get an error when trying to login with phpLDAPadmin  "Notice: Undefined variable: _SESSION in ..". 
For me this solution from StackOverflow solved the problem:

"Just add the user wich is running Apache2 (or php5-fpm!) to the system group "www-data" (debian) and restart services apache AND if used php5-fpm both.
Get the User apache is running as:

~# sed -rn 's/^User (.+)/\1/p' /etc/apache2/apache2.conf"

Using phpLDAPadmin I've created two groups "admin" and "regular_users" and also I've created some users allocated to these two groups.

2. Modify SimpleLDAPphp to use OpenLDAP


The documentation for using LDAP authentication is found here: https://simplesamlphp.org/docs/stable/ldap:ldap

My settings are:


Select LDAP authentication to be used from   /metadata/saml20-idp-hosted.php

 /* 
         *Authentication source to use. Must be one that is configured in
* 'config/authsources.php'.
*/
'auth' => 'example-ldap',

3. Modify Symfony app

In the current Symfony application I am expecting an attribute roles containing an array of roles. From LDAP I will receive different attributes, one of them is gidNumber, which is a number identifying a group. My current groups: admin and regular_users have gidNumber 500 and 501.
I will be using these gidNumbers to correctly create roles in the Symfony application.

The changes to be made are done in the UserCreator class:


Of course you need to change these mappings to fit your situation.

4. Test

First make sure to delete any sessions and cookies. After that try to access the secure route from consumer1.local, login with any user from LDAP and you should be redirected to secure area. Check in database if the user and user roles were created correctly.

Web SSO with Symfony and SimpleSAMLphp

1. Single sign on (SSO)


Sooner or later web development teams face one problem: you have developed an application at domain X and now you want your new application at domain Y to use the same login information as the other domain. In fact, you want more: you want users who are already logged-in at domain X to be already logged-in at domain Y. This is what SSO is all about. (source: https://auth0.com/blog/what-is-and-how-does-single-sign-on-work )

2. Concepts:


Identity Provider (IdP)  - is responsible for (a) providing identifiers for users looking to interact with a system, (b) asserting to such a system that such an identifier presented by a user is known to the provider, and (c) possibly providing other information about the user that is known to the provider.

ServiceProvider (SP) - it can be any application. In order to access the secure areas of the application the user need to be authenticated and authorized by a IdP

SAML  - Security Assertion Markup Language (SAML, pronounced sam-el) is an XML-based, open-standard data format for exchanging authentication and authorization data between parties, in particular, between an identity provider and a service provider. SAML is a product of the OASIS Security Services Technical Committee.

3. The flow


- the USER  requests a secure area from ServiceProvider
- if the user is not authenticated it is redirected to IdP with some information about SP
- the USER fills his credentials on IdP and after successful authentication is redirected back to SP
- based on the answer from IdP the SP creates the a new user or identify an existing one in his own database and creates a session.

4. Tools


SimpleSAMLphp - it can be used both as IdP and SP, I will be using it only as IdP
lightsaml/SP-Bundle - Symfony Bundle implementing the ServiceProvider

5. Implementation


5.1. Create and configure IdP

Install simpleSAMLphp following the documentation. I've created a virtualhost idp.local and the simpleSAMLphp is available at : http://idp.local/simplesamlphp

Next step is to configure simleSAMLphp as IdentityProvider following the quick guide using the exampleauth:UserPass authentication method.

I'll modify the defined users in config/authsources.php, by replacing the attribute "eduPersonAffilication" with 'roles':

      'student:studentpass' => array(
             'uid' => array('student'),
             'roles' => array('ROLE_USER', 'ROLE_SEF'),
         ),

To make it clear, you have just created a user "student" with password "studentpass". 
Skip the step 6 from the quick guide.
At the step 7 you need to enter the information about ServiceProviders. At this moment they do not exist, but they will, so you can fill the following 2 service providers:

$metadata['http://consumer1.local/saml'] = array(
    'AssertionConsumerService' => 'http://consumer1.local/app_dev.php/saml/login_check',
    'SingleLogoutService'      => 'https://consumer1.local/app_dev.php/logout',
    'simplesaml.nameidattribute' => 'uid',
);

$metadata['http://consumer2.local/saml'] = array(
    'AssertionConsumerService' => 'http://consumer2.local/app_dev.php/saml/login_check',
    'SingleLogoutService'      => 'https://consumer2.local/app_dev.php/logout',
    'simplesaml.nameidattribute' => 'uid',
);

I called them consumers because they consume the authentication service provided by IdP.
Skip steps from 8 to 11.

5.2 Create and configure SPs

Install Symfony and create a virtual host consumer1.local
We will be following the lightsaml/SP-Bundle documentation found here.

At step 7 configure your own entity_id and use the previously created IdP.

 -  First go to your IdP, it should be idp.local/simplesamlphp. Click on the Federation tab, you should see somewhere  SAML 2.0 IdP Metadata. Click on [Show metadata], copy the XML and createa file in your Symfony app: /src/AppBundle/Idp/idp.local.xml and paste the XML.
-  edit app/config.yml



At step 9 (and 11), please see below my security.yml:





Where /secure is a  just route I created for testing purpose. 

Step 10. Basically if the user logged in IdP does not exist in the database of the SP, in this case consumer1.local, it needs to be created.  For this reason a UserCreator class is made. In the documentation this class is able to identify the user id (uid) using a "username mapper" service. I will enhance that by adding an "attributes" mapper which will create an array with the other attributes passed from IdentityProvider. I want to pass a list of roles from IdP to SP.  If you remember I've added a list of 'roles' to my 'student' user. Below you can find a gist with the code for the AttributeMapper










 


Declare the attribute mapper service in app/services.yml:


attribute_mapper:
   class: AppBundle\Security\User\AttributeMapper

Modify the UserCreator class:



Declare the User Creator service injecting in it our Attribute Mapper service:

    user_creator:
        class: AppBundle\Security\User\UserCreator
        arguments:
            - "@=service('doctrine').getManager()"
            - "@lightsaml_sp.username_mapper.simple"
            - "@inno.attribute_mapper"
Also inject the attribute mapper service into the security.authentication.provider.lightsaml_sp service:



    security.authentication.provider.lightsaml_sp:
        class: LightSaml\SpBundle\Security\Authentication\Provider\LightsSamlSpAuthenticationProvider
        arguments:
            - ~ # provider key
            - ~ # user provider
            - ~ # force
            - "@security.user_checker"
            - "@lightsaml_sp.username_mapper.simple" # username mapper
            - ~ # user creator
            - "@inno.attribute_mapper" # attribute mapper
            - ~ # token factory
        abstract: true

5.3 Put your application at work

In browser try to access consumer1.local/app_dev.php/secure, because you are not authenticated
 you should be redirected to http://consumer1.local/app_dev.php/saml/discoveryClick on your IdP,
 you will be redirected to the IdP site where you will fill your user and password (student and 
studentpass. After that you are redirected to the secure page on cosumer1.local.You can check your
 database to see the newly created user. Now copy your Symfony installation and make another 
virtual host consumer2.local. Open the browser and try to access: consumer2.local/app_dev.php/securePick
 your IdP from discovery page, because you are already logged on IdP you will not be asked for 
password again, instead you are redirected to the secure page. Victory!In the next blog post I will
 investigate integrating LDAP with SimpleSAMLphp.







Wednesday, September 14, 2016

Using Silex and Symfony together in same application

I am testing the idea of having an application using both Silex and Symfony.

Why ?

Maybe because I want for some parts of the application to be really fast, and for the most of the application to enjoy the productivity of using a full stack framework and have access to all the third party bundles.

Of course having 2 application may introduce other kind of problems.

How my application should work:

- receive request
- fast Silex kernel handles the request.
     - if the URL request is not matching any of the routes from Silex, it will return a response with status 404
    -  if the URL is matching a route, execute controller and get response
- check what is the response from Silex
    - if the response has status 404 pass it to the Symfony kernel
   -  otherwise return response

Using the 2 frameworks together is possible because they both use the same abstraction for HTTP Request and Response and the HttpKernel.
There is a project called StackPHP that promotes interoperability between applications based on the HttpKernelInterface.

Implementation:


Create a probject folder under /var/www/public called double: /var/www/public/double
I will be using Symfony Standard Edition and Silex Skeleton project by Fabien Potencier: https://packagist.org/packages/fabpot/silex-skeleton

Install using Composer in two different subfolders:

   composer create-project fabpot/silex-skeleton  silex  ~2.0@dev
   composer create-project symfony/framework-standard-edition symf


Create a virtual host that will point to /var/www/public/double/symf/web
You may want to first save Symfony app.php and app_dev.php before editing them:
app_dev.php :

<?php

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Debug\Debug;
use Silex\Application;



/**
 * @var Composer\Autoload\ClassLoader $loader
 */
$loader = require __DIR__.'/../app/autoload.php';
require_once __DIR__.'/../../silex/vendor/autoload.php';

Debug::enable();

//create request object
$request = Request::createFromGlobals();

//initialize Silex app
$app = require __DIR__.'/../../silex/src/app.php';
require __DIR__.'/../../silex/config/dev.php';
require __DIR__.'/../../silex/src/controllers.php';
//handle request with Silex app
$response = $app->handle($request); if ($response->getStatusCode() === 404) {
    //initialize Symfony app and handle request
    $kernel = new AppKernel('dev', true);
    $kernel->loadClassCache();
    $response = $kernel->handle($request);
    $response->send();
    $kernel->terminate($request, $response);

} else {
    $response->send();
    $app->terminate($request, $response);
}


Into app.php I've added microtime() function to get the loading time in production as I do not have access to the Web Profiler.


<?php

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Silex\Application;

$time_start = microtime(true);
/**
 * @var Composer\Autoload\ClassLoader $loader
 */
$loader = require_once __DIR__.'/../app/autoload.php';
require_once __DIR__.'/../../silex/vendor/autoload.php';


//create request object
$request = Request::createFromGlobals();

//initialize Silex app
$app = require __DIR__.'/../../silex/src/app.php';
require __DIR__.'/../../silex/config/prod.php';
require __DIR__.'/../../silex/src/controllers.php';

$response = $app->handle($request);

if ($response->getStatusCode() === 404) {


    $kernel = new AppKernel('prod', false);
    $kernel->loadClassCache();
    $response = $kernel->handle($request);
    $response->send();
    $kernel->terminate($request, $response);

    $time_end = microtime(true);
    $time = $time_end - $time_start;

    echo "Execution time: $time seconds\n";

} else {
    $response->send();
    $app->terminate($request, $response);

    $time_end = microtime(true);
    $time = $time_end - $time_start;

    echo "Execution time: $time seconds\n";
}


I did some simple tests using the production env (app.php) and generally Silex is 3 times faster than Symfony, which is no surprise as it is lighter.
In Silex I used Doctrine DBAL (not ORM), the provider it is included in the Silex Skeleton project, just needs to be configured in src/app.php:



$app->register(new DoctrineServiceProvider(), array(
    'dbs.options' => array (
        'localhost' => array(
            'driver'    => 'pdo_mysql',
            'host'      => 'localhost',
            'dbname'    => 'symfony',
            'user'      => 'root',
            'password'  => 'root',
            'charset'   => 'utf8',
        )
    ),
));

Thursday, September 8, 2016

Multilingual website with Symfony

If you want to create a multilingual website, Symfony together with the community bundles have the tools for a fast set up.

When creating a multilingual website we need to take in account translating:

  1. Specific routes per language
  2. Menus, labels and forms
  3. The content of the website
  4. Extra: translate FOSUserBundle

1. Routes 

It may be tempting to use the same URL to display a resource in different languages based on the user's locale. For example, http://www.example.com/contact could show content in English for one user and French for another user. Unfortunately, this violates a fundamental rule of the Web: that a particular URL returns the same resource regardless of the user. To further muddy the problem, which version of the content would be indexed by search engines?
A better policy is to include the locale in the URL. This is fully-supported by the routing system using the special _locale parameter, see documentation:

The term locale refers roughly to the user's language and country. It can be any string that your application uses to manage translations and other format differences (e.g. currency format). The ISO 639-1 language code, an underscore (_), then the ISO 3166-1 alpha-2 country code (e.g. fr_FR for French/France) is recommended.

In config.yml add the following:

parameters:    locale: en_GB
    app.locales: en_GB|es_ES|fr_FR

framework:
    translator:  { fallbacks: ["%locale%"] }

By declaring the list of accepted as parameter you can easily use it in all routes declarations:

/** 
 * @Route("/{_locale}/product",  
 *     name="product",  
 *     requirements={ "_locale" = "%app.locales%" }) 
 */
public function productAction(Request $request)

By doing this, a route will look like this : www.mysite.com/en_GB/product

 2. Translations - labels, menus etc.

Translation of text is done through the translator service (Translator). To translate a block of text (called a message), use the trans() method, or in Twig using trans and transchoice tags.
The recommended way is to have message placeholders which will be translated using a translation file (one per each language).  An example would be a menu button that in english should show "About". We can create a placeholder named:  "menu.about" which will be translated in Twig:

     {{ 'menu.about'|trans }}

The translation files can have different formats (like YAML of XLIFF) and they live usually in app/Resources/translations, or under Resources/translations in your bundle.

I will be using YAML,  so I will create the files "messages.en_GB.yml" , "messages.es_ES.yml" etc.
Inside I add the placeholder and the translation:

button.product.view_detail : View detail

3. Content translation

One solution would be to create yourself the entities and handle the process. Imagine having a Product entity containing fields that do not need translation, like Id, Price and another entity ProductTranslation containing translatable fields like Name and Description. These two will be in a relation (example: OneToMany).

Another way is to use some community bundles that handle these process for us. I will be using KnpLabs/DoctrineBehaviors  and A2Lix TranslationBundle

Install DoctrineBehaviors and add translatable: true to config.yml  :

knp_doctrine_behaviors:    ...
    translatable:   true


Create your entities following the tutorial from documentation. Be careful that there are two different traits to be used in Entity and EntityTranslation classes.Do not add id field to the EntityTranslation class as it will be taken care by the traits. Update database structure:  php bin/console doctrine:schema:update  --force
I am using DoctrineFixturesBundle to add some data. For adding an apple(fruit) product the code is:


$product = new Product();
$product->translate('en')->setName('Apple ');
$product->translate('es')->setName('Manzana ');
$product->translate('en')->setDescription('Sweet apple');
$product->translate('es')->setDescription('Manzana dulce ');
$product->setImage('apple.jpeg');
$product->setCurrency($currency);
$product->setPrice(1.00);
$product->setActive('1');
$manager->persist($product);
$product->mergeNewTranslations();



In the database, I have a product_translation table where the information is saved. 
In controller I am retrieving the product object just as usual:


    $product = $em->getRepository('AppBundle:Product')->find($id);

In order to display the translatable fields in the current language in Twig I use:

    {{ product.translate(app.request.locale).name }}


For handling translation from the website interface you can use A2Lix TranslationBundle which will create a form with tabs for each translation (I've installed the version 3.x)

I've generated a ProductType class and added another line in code after adding my non translatable fields:  


use A2lix\TranslationFormBundle\Form\Type\TranslationsType;
...
$builder    ->add('active')
    ->add('price')
    ));
$builder->add('translations', TranslationsType::class);


4. Extra: translate FOSUserBundle

FOSUserBundle is one of the most popular Symfony bundles. You can easily activate translation by following the instructions from the link below:

https://codereviewvideos.com/course/getting-started-with-fosuserbundle/video/translations-and-internationalisation-in-fosuserbundle