SmallOrmBundle

From small iceberg
Jump to navigation Jump to search

What is SmallOrmBundle ?

SmallOrmBundle is a small ORM for symfony.

The goal of this ORM is to access data with better performances than lasy loading.

Repository

The source code is available on githut : https://github.com/sebk69/SebkSmallOrmBundle

Basic Principles

This ORM is build over two basic principles :

  • No lazy loading : The result objects structure is what you've asked in request with dependecies that you requested. Why ? The lazy loading do requests each time you ask access for first time of a dependency. More the database is complex, less performance you will have. And if you have strong batch to launch : I expect you have a strong server to do that !
  • What you get is what you have asked : The objects dependencies loaded is not the reflect of absolutes relations. If you've selected in where clause a part of the list of a dependency, only this part is loaded and is accessible.

Creating your first DAO

Here is your first DAO :

<?php

namespace Sebk\SmallMusicMomentBundle\Dao;

use Sebk\SmallOrmBundle\Dao\AbstractDao;

class TitleDao extends AbstractDao
{
    protected function build()
    {
        $this->setDbTableName("title")
            ->setModelName("Title")
            ->addPrimaryKey("id", "id")
            ->addField("name", "name")
            ->addField("path", "path")
            ->addField("tags", "tags")
            ->addField("id_library", "idLibrary")
            ->addField("id_artist", "idArtist")
            ->addField("id_album", "idAlbum")
            ->addToOne("titleAlbum", ["id" => "idAlbum"], "Album")
            ->addToOne("titleArtist", ["id" => "idArtist"], "Artist")
            ->addToOne("titleLibrary", ["id" => "idLibrary"], "Library")
        ;
    }
}

You can note that your DAO always extends AbstractDao. The method "build" is required and must set a few parameters :

$this->setDbTableName("project");

This statement define that your dao concern the table "project" in database.

$this->setModelName("Project");

This is the class name of your corresponding model.

$this->addPrimaryKey("id", "id");

Here is your primary key : first parameter for db name and second parameter for the name of correponding property of your model class.

$this->addField("branch_prefix", "branchPrefix");

Here is a simple field : as for the primary key, first parameter for db name and second parameter for the name of correponding property of your model class.

$this->addToOne("leader", array("leaderId" => "id"), "User", "SebkSmallUserBundle");

This statement define a to one relation. The first parameter defined the property that will map the sub object. The second parameter is an array to define the foreign keys of relation : The key of array is the from field and the second key is the field of foreign model. The Third parameter is the name of destination model The last (optionnal) parameter is the bundle of destination model. If ommited, the ORM look in the same bundle. Yes you can do requests cross bundle and corss databases in SmallOrm !

$this->addToMany("childs", array("foreignKey" => "childKey"), "ChildModel");

Same as addToOne but with many childs

Creating your first request

Good practice is creating requests in your DAO object :

<?php

namespace Sebk\ProjectBundle\Dao;

use Sebk\SmallOrmBundle\Dao\AbstractDao;

class Project extends AbstractDao {
    /**
     * Build structure of DAO
     * @throws \Sebk\SmallOrmBundle\Dao\DaoException
     */
    protected function build() {
        $this->setDbTableName("project");
        $this->setModelName("Project");
        $this->addPrimaryKey("id", "id");
        $this->addField("name", "name");
        $this->addField("branch_prefix", "branchPrefix");
        $this->addField("leader_user_id", "leaderId");
        $this->addToOne("leader", array("leaderId" => "id"), "User", "SebkSmallUserBundle");
    }
   
    /**
     * List my projects
     * @param mixed $userId
     * @return array
     */
    public function getMyProjects($userId) {
        $query = $this->createQueryBuilder("project")
                ->innerJoin("project", "leader")->endJoin();
        $query->where()
                ->firstCondition($query->getFieldForCondition("leaderId", "project"), "=", ":userId")
                ->endWhere()
                ->setParameter("userId", $userId);
       
        return $this->getResult($query);
    }
}
First step is creating the query builer object
$this->createQueryBuilder("project")

      This statement create a query on this model aliased by "project". Traducted to sql this initiate this query statement :

select * from project as project
Second step is join models that you want to map in your result objects
$query->innerJoin("project", "leader")->endJoin();

      First parameter is model who own the relation (see the addToOne statement of DAO) and the second is the relation name.

      At this step, our sql statement will be :

select * from project as project inner join user as leader on project.leader_user_id = leader.id
We can now add or condition

      First create where object :

$whereObject = $query->where();

      We can create our first condition :

$whereObject->firstCondition($query->getFieldForCondition("leaderId", "project"), "=", ":userId")

      $query->getFieldForCondition("leaderId", "project") return the field object "leaderId" from model aliased by "project".

      Our sql staement is now :

select * from project as project inner join user as leader on project.leader_user_id = leader.id
where project.leaderId = :userId

      Just set parameter

$query->setParameter("userId", $userId);

    Let execute query :

$this->getResult($query);

      Who get this result :

      [
          {
              "id": 10,
              "name": "my project 1",
              "branchPrefix": "dev1",
              "leaderId": 3,
              "leader": {
                  "id": 3
                  "nickname": "sebk69"
              }
          },
          {
              "id": 15,
              "name": "my project 2",
              "branchPrefix": "dev2",
              "leaderId": 3,
              "leader": {
                  "id": 3
                  "nickname": "sebk69"
              }
          }
      ]

     

      These result objects are models object and you can access properties and subobjects with getters and setters :

$name0 = $resultProjects[0]->getName();
$leader0Nickname = $resultProjects[0]->getLeader()->getNickname();

     

Automatic generation

You can generate dao and models files from database.

    First add table. In our example we will add table "title" from default connection in "SebkSmallMusicMomentBundle" :

      $ bin/console sebk:small-orm:add-table
      Connection [default] ?
      Bundle [SebkSmallMusicMomentBundle] ?
      Database table [all] ? title

     

    Here is generated files :

   In folder "Dao"

      <?php
      namespace Sebk\SmallMusicMomentBundle\Dao;
      use Sebk\SmallOrmBundle\Dao\AbstractDao;

      class TitleDao extends AbstractDao
      {
          protected function build()
          {
              $this->setDbTableName("title")
                  ->setModelName("Title")
                  ->addPrimaryKey("id", "id")
                  ->addField("name", "name")
                  ->addField("path", "path")
                  ->addField("tags", "tags")
                  ->addField("id_library", "idLibrary")
                  ->addField("id_artist", "idArtist")
                  ->addField("id_album", "idAlbum")
                  ->addToOne("titleAlbum", ["id" => "idAlbum"], "Album")
                  ->addToOne("titleArtist", ["id" => "idArtist"], "Artist")
                  ->addToOne("titleLibrary", ["id" => "idLibrary"], "Library")
              ;
          }
      }

     

      And in folder "Model"

      <?php

      namespace Sebk\SmallMusicMomentBundle\Model;

      use Sebk\SmallOrmBundle\Dao\Model;

      class TitleModel extends Model
      {
      }

Database layers

Database layers allow you to manage database modifications across versions of your code and between multiples bundles.

For example, we have two bundles :

    • SmallMusicMomentBundle

    • SmallUserBundle

The SmallUserBundle don't know SmallMusicMoment bundle and it's define two tables which are used by SmallMusicMomentBundle.

The user table and the user_role table. These tables can be altered between versions of SmallMusicMomentBundle.

The goal of database layers is to maintain the build of database over versions and bundle without thinking of the work of other contributers.

First, we define SmallUserBundle first version layer. Here is the directory structure :

    • We have the "SmallUser-1" folder which define the name of layer.

    • The sql scripts to create tables are in "scripts" directory. They will be executed in alphabetical order.

    • The configuration is in the "config.yml" file.

In this first example, the "config.yml" file contains only the connection in which the scripts will be executed :

connection: default

The second layer is in SmallMusicMomentBundle :

In the "SmallMusicMoment-1" layer, the config file define the dependency of the SmallUserBundle layer :

connection: default
depends:
    - SmallUser-1@SebkSmallUserBundle

This mean that "SmallMusicMoment-1" can't be executed before layer "SmallUser-1" has been executed.

Now we can execute layers :

$ bin/console sebk:small-orm:layers-execute
Execute layer SmallUser-1... done
Execute layer SmallMusicMoment-1... done

In addition, you can add selections based on values in 'paramters.yml' to create specific layers on installation :

connection: default
depends:
    - SmallUser-1@SebkSmallUserBundle
required-parameters:
    client_name: "Acme company"

With that configuration, the layer will not be executed until the parameter "client_name" has the value "Acme company".

Validate and persist model

SmallOrm come with helper class to validate your model. Simply add a class that extends AbstractValidator with a validate method.

Here is an example for model BareCode, the field code must be unique :

<?php

namespace Sebk\SmallMusicMomentBundle\Validator;

use Sebk\SmallOrmBundle\Validator\AbstractValidator;

class BareCode extends AbstractValidator
{
    /**
     * Validate
     * @return boolean
     */

    public function validate()
    {
        $this->message = "";
        $valid = true;
        if (!$this->testUnique("code")) {
            $this->message = "The bare code must be unique";

            $valid = false;

        }

        return $valid;
    }
}

And here is the persist process in your controller or service :

<?php

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * @route("/api/barecodes")
 */

class BareCodeController extends Controller
{
    /**
     * @route("")
     * @method({"POST"})
     */
    public function saveAction(Request $request)
    {
        // Get DAO
        $daoBareCode = $this->get("sebk_small_orm_dao")->get("SmallMusicMomentBundle", "BareCode");

        // Create model from POST data
        $model = $daoBareCode->makeModelFromStdClass(json_decode($request->getContent()));

        // Validate
        if ($model->getValidator()->validate()) {
            // And persist
            $model->persist();
        } else {
            // Validation failed, send validator messages as response
            return new Response(json_encode($model->getValidator()->getMessage()), 400);
        }
 
       // Successfull, send modified ressource
        return new Response(json_encode($model));
    }
}

Delete a model

You can use method delete for remove associated record in database :

$model->delete();

Backup model

You can take snapshot of model with backup method to consult old values later. For example :

// Backup model
$user->backup();

// Change user name
$user->setName("New name");

// Test if modified
if($user->getName() != $user->getBackup()->name) {
    echo "The name have been updated";
}

By default, this method create only backup on the model and not on dependencies. To backup also dependencies you can specify true as parameter :

// Backup model deeply
$user->backup(true);

// Change group name
$user->getGroup()->setName("New group name");

// Test if modified
if($user->getGroup()->getName() != $user->getGroup()->getBackup()->name) {
    echo "The group name has been updated";
}

// Or test any modification on model since last backup
if($user->modifiedSinceBackup()) {
    echo "The user has been modified";
}

Notes :

    • The backup is serialised on json_encode

    • The metadata is also saved

It is also possible to rebuild old model :

// Set name of user
$user->setName("Seb");

// Backup model
$user->backup();

// Change name
$user->setName("Opheli");

// Restore backup in $user2
$user2 = $dao->makeModelFromStdClass($user->getBackup());

// The two models names are ...
var_dump($user->getName()); // Output : Opheli
var_dump($user2->getName()); // Output : Seb

Transactions

Transactions are usable by small orm from connections. You can :

    • Start transaction

    • Commit

    • Rollback

If the script end without commit, a rollback is done.

Here is an example from our code barre controller :

<?php

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * @route("/api/barecodes")
 */

class BareCodeController extends Controller
{
    /**
     * @route("")
     * @method({"POST"})
     */
    public function saveAction(Request $request)
    {
        // Get default connection
        $connection = $this->get("sebk_small_orm_connections")->get("default");

        // Starting transaction
        $connection->startTransaction();

        // Get DAO
        $daoBareCode = $this->get("sebk_small_orm_dao")->get("SmallMusicMomentBundle", "BareCode");

        // Create model from POST data
        $model = $daoBareCode->makeModelFromStdClass(json_decode($request->getContent()));

        // Validate model
        if ($model->getValidator()->validate()) {
            try {
                // Persist and commit
                $model->persist();
                $connection->commit();
            } catch (\Exception $e) {
                // Rollback if an afterSave method throw an exception
                $connection->rollback();
            }
        } else {
            // Validation failed, send validator messages as response
            return new Response(json_encode($model->getValidator()->getMessage()), 400);
        }

        // Successfull, send modified resource
        return new Response(json_encode($model));
    }
}

Configuration

Here is the code to put in your app/config/config.yml :

sebk_small_orm:
    connections:
        default:
            type: mysql
            host: %db_host%
            database: %database_name%
            encoding: utf8
            user:     %db_user%
            password: %db_password%

    bundles:
        AppAcmeBundle:
            connections:
                default:
                    dao_namespace: App\AcmeBundle\Dao
                    model_namespace: App\AcmeBundle\Model
                    validator_namespace: App\AcmeBundle\Validator

How to get it

The source code is available on githut : https://github.com/sebk69/SebkSmallOrmBundle

To use it :

Require the bundle with composer:

composer require sebk/small-orm-bundle

Register the bundle in app/AppKernel.php:

public function registerBundles()

{

    return array(

        // ...

        new Sebk\SmallOrmBundle\SebkSmallOrmBundle(),

    );

}

Author

Sébastien Kus
Web developer at La Bécanerie