Previous Next
PHP/Symfony Data Jukebox Tutorial Bundle

PHP/Symfony Data Jukebox Tutorial Bundle - The Tutorial

Learn to forget all about views and concentrate entirely on the model.

Table of Contents

The philosophy behind the Data Jukebox Bundle

The Data Jukebox Bundle sticks to Symfony philosophy as much as possible. Shortly put, one defines Entities, which are then manipulated - in the Controller - thanks to a corresponding (standard or customized) Repository. Rendering is achieved via Template while localization is done thanks to translation files.

The Data Jukebox Bundle simply adds Properties to the picture, which corresponds to a PHP class where specific methods - matching the PropertiesInterface definition - allow to retrieve additional meta-data about the corresponding Entity and construct - entirely automatically - the views for the common CRUD (Create-Read-Update-Delete) operations.

The one aspect where the Data Jukebox Bundle differs from Symfony is the way it deals with joined entities. Shortly put: it doesn't! Read operations can not use joined entities the way Symfony does. The idea behind this design decision is that joining entities fields (aka. tables columns) for read purposes is something that is better achieved in SQL views, with all the power of the database engine backing the operation. Thus, while relationships can still be defined and used according to Symfony philosophy when dealing with forms - even within the Data Jukebox Bundle realm - they must be "resolved" at the SQL (view) level when dealing with read actions, with joined columns being returned as scalar values rather than entities (objects). This distinction between the two purposes is implemented using entity inheritance, where an abstract MappedSuperclass acts as parent for one standard, table-backed Entity and another joined, view-backed (View)Entity.

A sample blog application

A sample blog application will be used to illustrate how to use the Data Jukebox Bundle.

Blog Category

To start with, we will define a Blog Category Entity:

  1. <?php
  2. namespace DataJukeboxTutorialBundle\Entity;
  3.  
  4. use Doctrine\ORM\Mapping as ORM;
  5. use DataJukeboxBundle\Annotations\Properties;
  6.  
  7. /** Blog category entity
  8.  * @ORM\Entity
  9.  * @ORM\Table(name="SampleBlogCategory")
  10.  * @Properties(propertiesClass="DataJukeboxTutorialBundle\Properties\SampleBlogCategoryProperties")
  11.  */
  12.   implements \DataJukeboxBundle\DataJukebox\PrimaryKeyInterface
  13. {
  14.  
  15.   /*
  16.    * PROPERTIES
  17.    ********************************************************************************/
  18.  
  19.   /** Primary key
  20.    * @var integer 
  21.    * @ORM\Column(name="pk", type="integer")
  22.    * @ORM\Id
  23.    * @ORM\GeneratedValue
  24.    */
  25.   protected $PK;
  26.  
  27.   /** Name
  28.    * @var string 
  29.    * @ORM\Column(name="Name_vc", type="string", length=100)
  30.    */
  31.   protected $Name;
  32.  
  33.   // ...
  34.  
  35.   /*
  36.    * METHODS
  37.    ********************************************************************************/
  38.  
  39.   // Setters/Getters/...
  40.  
  41.  
  42.   /*
  43.    * METHODS: PrimaryKeyInterface
  44.    ********************************************************************************/
  45.  
  46.   public function getPrimaryKey()
  47.   {
  48.     return array('PK' => $this->PK);
  49.   }
  50.  
  51. }

The only way it differs from a standard Symfony entity:

  • it implements the Data Jukebox PrimaryKeyInterface and the corresponding getPrimaryKey method
  • it declares a corresponding Properties class, using the annotation @Properties(propertiesClass="...")
  • We will cover those changes when dealing with the application core entity (see below).

    Blog Entry

    We will now define the Blog Entry resources.

    Given the many-to-one relationship with the Blog Category entity and Data Jukebox way of dealing with those, we must first define the parent abstract SuperClass:

    1. <?php
    2. namespace DataJukeboxTutorialBundle\Entity;
    3.  
    4. use Doctrine\ORM\Mapping as ORM;
    5.  
    6. /** Blog entry superclass
    7.  * @ORM\MappedSuperclass
    8.  */
    9.   implements \DataJukeboxBundle\DataJukebox\PrimaryKeyInterface
    10. {
    11.  
    12.   /*
    13.    * PROPERTIES
    14.    ********************************************************************************/
    15.  
    16.   /** Primary key
    17.    * @var integer 
    18.    * @ORM\Column(name="pk", type="integer")
    19.    * @ORM\Id
    20.    * @ORM\GeneratedValue
    21.    */
    22.   protected $PK;
    23.  
    24.   /** Title
    25.    * @var string 
    26.    * @ORM\Column(name="Title_vc", type="string", length=100)
    27.    */
    28.   protected $Title;
    29.  
    30.   /** Date
    31.    * @var \DateTime 
    32.    * @ORM\Column(name="Date_d", type="date")
    33.    */
    34.   protected $Date;
    35.  
    36.   // ...
    37.  
    38.   /*
    39.    * METHODS
    40.    ********************************************************************************/
    41.  
    42.   // Getters/...
    43.  
    44.  
    45.   /*
    46.    * METHODS: PrimaryKeyInterface
    47.    ********************************************************************************/
    48.  
    49.   public function getPrimaryKey()
    50.   {
    51.     return array('PK' => $this->PK);
    52.   }
    53.  
    54. }

    Then we can define the corresponding "standard", table-backed Entity (for create/update purposes):

    1. <?php
    2. namespace DataJukeboxTutorialBundle\Entity;
    3.  
    4. use Doctrine\ORM\Mapping as ORM;
    5. use DataJukeboxBundle\Annotations\Properties;
    6.  
    7. /** Blog entry entity (for create/update purposes)
    8.  * @ORM\Entity
    9.  * @ORM\Table(name="SampleBlogEntry")
    10.  * @Properties(propertiesClass="DataJukeboxTutorialBundle\Properties\SampleBlogEntryProperties")
    11.  */
    12. {
    13.  
    14.   /*
    15.    * PROPERTIES
    16.    ********************************************************************************/
    17.  
    18.   /** Category (entity)
    19.    * @var SampleBlogCategoryEntity 
    20.    * @ORM\ManyToOne(targetEntity="SampleBlogCategoryEntity")
    21.    * @ORM\JoinColumn(name="Category_fk", referencedColumnName="pk")
    22.    */
    23.   protected $Category;
    24.  
    25.  
    26.   /*
    27.    * METHODS
    28.    ********************************************************************************/
    29.  
    30.   // Setters/Getters/...
    31.  
    32. }

    And its joined, view-backed Entity (for read purposes), where the Category is no longer an entity/object, but a scalar (string) corresponding to the category name:

    1. <?php
    2. namespace DataJukeboxTutorialBundle\Entity;
    3.  
    4. use Doctrine\ORM\Mapping as ORM;
    5. use DataJukeboxBundle\Annotations\Properties;
    6.  
    7. /** Blog entry (view) entity (for reading purposes)
    8.  * @ORM\Entity
    9.  * @ORM\Table(name="SampleBlogEntry_view")
    10.  * @Properties(propertiesClass="DataJukeboxTutorialBundle\Properties\SampleBlogEntryProperties")
    11.  */
    12. {
    13.  
    14.   /*
    15.    * PROPERTIES
    16.    ********************************************************************************/
    17.  
    18.   /** Category (foreign key)
    19.    * @var integer 
    20.    * @ORM\Column(name="Category_fk", type="integer")
    21.    */
    22.   protected $CategoryFK;
    23.  
    24.   /** Category (name)
    25.    * @var string 
    26.    * @ORM\Column(name="Category_vc", type="string", length=100)
    27.    */
    28.   protected $Category;
    29.  
    30.  
    31.   /*
    32.    * METHODS
    33.    ********************************************************************************/
    34.  
    35.   // Getters/...
    36.  
    37. }

    Primary Key Interface

    The Primary Key Interface and its sole getPrimaryKey() method are here to simplify the process of retrieving the entity primary key name(s)/value(s) pair(s). If an entity has more than one field acting as identifier - thus corresponding to a composite primary key - one must just make sure to include all identifier fields in the returned array.

    Properties

    The entity associated Properties is where all the magic of the Data Jukebox Bundle takes its root:

    1. <?php
    2. namespace DataJukeboxTutorialBundle\Properties;
    3.  
    4.   extends \DataJukeboxBundle\DataJukebox\Properties
    5.   implements \DataJukeboxBundle\DataJukebox\FormatInterface
    6. {
    7.  
    8.   /*
    9.    * CONSTANTS
    10.    ********************************************************************************/
    11.  
    12.   /** Administration authorization level
    13.    * @var integer 
    14.    */
    15.   const AUTH_ADMIN = 9;
    16.  
    17.  
    18.   /*
    19.    * METHODS: Properties(Interface)
    20.    ********************************************************************************/
    21.  
    22.   // Return whether the defined action is authorized
    23.   // at the defined authorization level
    24.   public function isAuthorized()
    25.   {
    26.     switch ($this->getAction()) {
    27.     case 'list':
    28.     case 'detail':
    29.       return true;
    30.     case 'insert':
    31.     case 'update':
    32.     case 'delete':
    33.       return $this->getAuthorization(>= self::AUTH_ADMIN;
    34.     }
    35.     return parent::isAuthorized();
    36.   }
    37.  
    38.   // Return the routes for the various CRUD operations
    39.   public function getRoutes()
    40.   {
    41.     // As per authorization level
    42.     if ($this->getAuthorization(>= self::AUTH_ADMIN{
    43.       switch ($this->getAction()) {
    44.       case 'list':
    45.         return array(
    46.           'detail' => array('SampleBlogEntry_view'),
    47.           'insert' => array('SampleBlogEntry_edit'),
    48.           'delete' => array('SampleBlogEntry_delete'),
    49.           'select_delete' => array('SampleBlogEntry_delete'),
    50.         );
    51.       case 'detail':
    52.         return array(
    53.           'list' => array('SampleBlogEntry_view'),
    54.           'update' => array('SampleBlogEntry_edit'),
    55.           'delete' => array('SampleBlogEntry_delete'),
    56.         );
    57.       }
    58.     }
    59.  
    60.     // Default
    61.     switch ($this->getAction()) {
    62.     case 'list':
    63.       return array(
    64.         'detail' => array('SampleBlogEntry_view'),
    65.       );
    66.     case 'detail':
    67.       return array(
    68.         'list' => array('SampleBlogEntry_view'),
    69.       );
    70.     }
    71.  
    72.     // Fallback
    73.     return parent::getRoutes();
    74.   }
    75.  
    76.   // Return the (localized) fields labels
    77.   public function getLabels()
    78.   {
    79.     return array_merge(
    80.       parent::getLabels(),
    81.       $this->getMeta('label''SampleBlogEntry')
    82.     );
    83.   }
    84.  
    85.   // Return the (localized) fields tooltips
    86.   public function getTooltips()
    87.   {
    88.     return array_merge(
    89.       parent::getTooltips(),
    90.       $this->getMeta('tooltip''SampleBlogEntry')
    91.     );
    92.   }
    93.  
    94.   // Return the fields allowed to query/display
    95.   public function getFields()
    96.   {
    97.     switch ($this->getAction()) {
    98.     case 'list':
    99.       return array('Title''Date''Category''Tags');
    100.     }
    101.     return array('Title''Date''Category''Content''Tags');
    102.   }
    103.  
    104.   // Return the fields to query/display by default
    105.   public function getFieldsDefault()
    106.   {
    107.     switch ($this->getAction()) {
    108.     case 'list':
    109.       return array('Title''Date''Category');
    110.     }
    111.     return parent::getFieldsDefault();
    112.   }
    113.  
    114.   // Return the fields that must be kept hidden
    115.   public function getFieldsHidden()
    116.   {
    117.     return array_merge(
    118.       array('PK''CategoryFK'),
    119.       parent::getFieldsHidden()
    120.     );
    121.   }
    122.  
    123.   // Return the fields that must be displayed or data-filled
    124.   public function getFieldsRequired()
    125.   {
    126.     switch ($this->getAction()) {
    127.     case 'list':
    128.     case 'detail':
    129.       return array('CategoryFK''Title');
    130.     case 'insert':
    131.     case 'update':
    132.       return array('Title''Date''Category''Content');
    133.     }
    134.     return parent::getFieldsRequired();
    135.   }
    136.  
    137.   // Return the fields that may not be modified
    138.   public function getFieldsReadonly()
    139.   {
    140.     switch ($this->getAction()) {
    141.     case 'update':
    142.       return array('Date');
    143.     }
    144.     return parent::getFieldsReadOnly();
    145.   }
    146.  
    147.   // Return the default value for specific fields
    148.   public function getFieldsDefaultValue()
    149.   {
    150.     return array(
    151.       'Date' => new \DateTime(),
    152.     );
    153.   }
    154.  
    155.   // Return the links to associate with each field
    156.   public function getFieldsLink()
    157.   {
    158.     switch ($this->getAction()) {
    159.     case 'list':
    160.       return array_merge(
    161.         array(
    162.           'Title' => array('path''SampleBlogEntry_view'array('_pk' => 'PK')),
    163.         ),
    164.         parent::getFieldsLink()
    165.       );
    166.     case 'detail':
    167.       return array_merge(
    168.         array(
    169.           'Category' => array('path''SampleBlogCategory_view'array('_pk' => 'CategoryFK')),
    170.         ),
    171.         parent::getFieldsLink()
    172.       );
    173.     }
    174.     return parent::getFieldsLink();
    175.   }
    176.  
    177.   // Return the fields that may be used for sorting list views
    178.   public function getFieldsOrder()
    179.   {
    180.     return array_merge(
    181.       array('Date'),
    182.       parent::getFieldsOrder()
    183.     );
    184.   }
    185.  
    186.   // Return the fields that may be used for filtering list views
    187.   public function getFieldsFilter()
    188.   {
    189.     return array('Title''Date''Category''Tags');
    190.   }
    191.  
    192.   // Return the fields that are used for searching data (in list views)
    193.   public function getFieldsSearch()
    194.   {
    195.     return array('Title''Content');
    196.   }
    197.  
    198.   // Return the additional links to create in views
    199.   public function getFooterLinks()
    200.   {
    201.     switch ($this->getAction()) {
    202.     case 'detail':
    203.       return array_merge(
    204.         array(
    205.           '_view_list' => array('path+query''SampleBlogEntry_view'null'⊛'),
    206.         ),
    207.         parent::getFooterLinks()
    208.       );
    209.     }
    210.     return parent::getFooterLinks();
    211.   }
    212.  
    213.   // Return the Twig template for each action
    214.   public function getTemplate()
    215.   {
    216.     switch ($this->getAction()) {
    217.     case 'list':
    218.       return '@DataJukeboxTutorial/list.html.twig';
    219.     case 'detail':
    220.       return '@DataJukeboxTutorial/detail.html.twig';
    221.     case 'insert':
    222.     case 'update':
    223.       return '@DataJukeboxTutorial/form.html.twig';
    224.     }
    225.     throw new \Exception(sprintf('Invalid action (%s)'$this->getAction()));
    226.   }
    227.  
    228.  
    229.   /*
    230.    * METHODS: FormatInterface
    231.    ********************************************************************************/
    232.  
    233.   // Custom formatting for specific fields
    234.   public function formatFields(array &$aRow$iIndex{
    235.     if (isset($aRow['Date'])) $aRow['Date_formatted'$aRow['Date']->format('r');
    236.   }
    237.  
    238. }

    Ok, this is a lengthy definition... but at the end of the day, it says all there is to know about the entity in order to create all views - for create/read/update/delete purposes - automatically, while respecting the business logic that backs the entity.

    Controller

    We can now create the Controller that will create all those views:

    1. <?php
    2. namespace DataJukeboxTutorialBundle\Controller;
    3.  
    4. use Symfony\Component\HttpFoundation\Request;
    5. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    6. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    7.  
    8.  
    9.   extends Controller
    10. {
    11.  
    12.   public function getAuthorization()
    13.   {
    14.     $oSecurityAuthorizationChecker $this->get('security.authorization_checker');
    15.     if ($oSecurityAuthorizationChecker->isGranted('ROLE_BLOG_ADMIN')) return SampleBlogEntryProperties::AUTH_ADMIN;
    16.     return SampleBlogEntryProperties::AUTH_PUBLIC;
    17.   }
    18.  
    19.   public function viewAction($_pkRequest $oRequest)
    20.   {
    21.     // Properties
    22.     $oDataJukebox $this->get('DataJukebox');
    23.     $iAuthorization $this->getAuthorization();
    24.     $oProperties $oDataJukebox->getProperties('DataJukeboxTutorialBundle:SampleBlogEntryViewEntity')
    25.                                 ->setAuthorization($iAuthorization)
    26.                                 ->setAction(is_null($_pk'list' 'detail')
    27.                                 ->setTranslator($this->get('translator'));
    28.     if (!$oProperties->isAuthorized()) throw new AccessDeniedException();
    29.  
    30.     // Browsing
    31.     $oBrowser $oProperties->getBrowser($oRequest);
    32.     if (is_null($_pkand !$oBrowser->getFieldsOrder()) $oBrowser->setFieldsOrder('Date_D');
    33.  
    34.     // Data query
    35.     $oRepository $oDataJukebox->getRepository($oProperties);
    36.     $oResult $oRepository->getDataResult($_pk$oBrowser);
    37.  
    38.     // Response
    39.     return $this->render(
    40.       $oProperties->getTemplate(),
    41.       array('data' => $oResult->getTemplateData())
    42.     );
    43.   }
    44.  
    45.   public function editAction($_pkRequest $oRequest)
    46.   {
    47.     // Properties
    48.     $oDataJukebox $this->get('DataJukebox');
    49.     $iAuthorization $this->getAuthorization();
    50.     $oProperties $oDataJukebox->getProperties('DataJukeboxTutorialBundle:SampleBlogEntryEntity')
    51.                                 ->setAuthorization($iAuthorization)
    52.                                 ->setAction(is_null($_pk'insert' 'update')
    53.                                 ->setTranslator($this->get('translator'));
    54.     if (!$oProperties->isAuthorized()) throw new AccessDeniedException();
    55.  
    56.     // Data query
    57.     $oRepository $oDataJukebox->getRepository($oProperties);
    58.     $oData is_null($_pknull $oRepository->getDataEntity($_pk);
    59.  
    60.     // Form resources
    61.     $oFormType $oDataJukebox->getFormType($oProperties);
    62.     $oForm $this->createForm($oFormType$oData);
    63.  
    64.     // Form handling
    65.     $oForm->handleRequest($oRequest);
    66.     if ($oForm->isValid()) {
    67.       $oData $oForm->getData();
    68.       $oEntityManager $oProperties->getEntityManager();
    69.       $oEntityManager->persist($oData);
    70.       $oEntityManager->flush();
    71.       return $this->redirectToRoute('SampleBlogEntry_view'$oFormType->getPrimaryKeySlug($oData));
    72.     }
    73.  
    74.     // Response
    75.     return $this->render(
    76.       $oProperties->getTemplate(),
    77.       array(
    78.         'form' => $oForm->createView(),
    79.         'data' => array('properties' => $oProperties->getTemplateData()),
    80.       )
    81.     );
    82.   }
    83.  
    84.   public function deleteAction($_pkRequest $oRequest)
    85.   {
    86.     // Properties
    87.     $oDataJukebox $this->get('DataJukebox');
    88.     $iAuthorization $this->getAuthorization();
    89.     $oProperties $oDataJukebox->getProperties('DataJukeboxTutorialBundle:SampleBlogEntryEntity')
    90.                                 ->setAuthorization($iAuthorization)
    91.                                 ->setAction('delete');
    92.     if (!$oProperties->isAuthorized()) throw new AccessDeniedException();
    93.  
    94.     // Data primary keys (potentially passed by HTTP POST)
    95.     if (is_null($_pk)) {
    96.       $asPK = \DataJukeboxBundle\DataJukebox\Browser::getPrimaryKeys($oRequest);
    97.     else {
    98.       $asPK array($_pk);
    99.     }
    100.  
    101.     // Data query
    102.     if (count($asPK)) {
    103.       $oRepository $oDataJukebox->getRepository($oProperties);
    104.       $oEntityManager $oProperties->getEntityManager();
    105.       foreach ($asPK as $sPK{
    106.         $oData $oRepository->getDataEntity($sPK);
    107.         $oEntityManager->remove($oData);
    108.       }
    109.       $oEntityManager->flush();
    110.     }
    111.  
    112.     // Response
    113.     return $this->redirectToRoute('SampleBlogEntry_view'$oRequest->query->all());
    114.   }
    115.  
    116. }

    ... and this is it!

    Tips and Tricks

    A few things are worth noting when using the Data Jukebox Bundle:

  • Primary Key (Route) Parameter: the primary key parameter in Data Jukebox routes and controllers MUST be _pk, even for entities that have a composite (multiple-fields) primary key.
  • Twig Template: you can customite the views as you deem fit, using (Symfony) form and (Data Jukebox) DataJukebox_list/DataJukebox_detail functions in your Twig templates (Note: PHP templates are NOT supported).
  • Localization: is achieved using Symfony standard translations files, along the handy getMeta method in your Properties; just make sure to specify the correct translation domain.
  • Previous Next
    PHP/Symfony Data Jukebox Tutorial Bundle

    Documentation generated on Wed, 15 Nov 2017 15:15:10 +0100 by phpDocumentor 1.4.4