Difference between revisions of "SmallOrmBundle"

From small iceberg
Jump to navigation Jump to search
Line 16: Line 16:
 
Here is your first DAO :
 
Here is your first DAO :
  
<code><?php
+
"""<?php
  
 
namespace Sebk\SmallMusicMomentBundle\Dao;
 
namespace Sebk\SmallMusicMomentBundle\Dao;
Line 40: Line 40:
 
        ;
 
        ;
 
    }
 
    }
}</code>
+
}"""
  
 
You can note that your DAO always extends AbstractDao. The method "build" is required and must set a few parameters :
 
You can note that your DAO always extends AbstractDao. The method "build" is required and must set a few parameters :

Revision as of 03:53, 5 October 2020

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.

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

      {

      }

     

    • After every alter table you can just run :

      $ bin/console sebk:small-orm:update-dao

     

      and the build method for all dao of added tables will be updated.

    • These two commands not alter code outside build method and your own methods are untouched.

> 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(),

    );

}

SmallKeyring is a small webapp self hosted to store confidential data like passwords.

1

2

3

4

5

    • Secret passphrase is optionnal

    • It is never stored and used to generate a private key that encrypt all your personnal data

    • Without entering the good password, even the owner of the site can't decrypt your personnal data

This webapp use SmallOrmBundle and symfony 4 to work.

If you want to test SmallKeyring or develop with it you can generate your localhost environnement in 6 commands lines on this github repo