Small-orm-swoft

From small iceberg
Jump to navigation Jump to search

What is small-orm-swoft ?

small-orm-swoft is a small ORM for Swoft.

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

It include a connector to use native Swoft connection pool in order to

Repository

The source code is available on github : https://github.com/sebk69/small-orm-swoft

The core package (small-orm-core) source code is available on github : https://github.com/sebk69/small-orm-core

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\SmallOrmCore\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\SmallOrmCore\Dao\AbstractDao;

class Project extends AbstractDao {
    /**
     * Build structure of DAO
     * @throws \Sebk\SmallOrmCore\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/swoft sebk:small-orm:add-table --connection default --bundle SebkSmallMusicMomentBundle --table title

     

    Here is generated files :

   In folder "Dao"

      <?php
      namespace Sebk\SmallMusicMomentBundle\Dao;

      use Sebk\SmallOrmCore\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\SmallOrmCore\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/swoft 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\SmallOrmCore\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 declare(strict_types=1);

namespace App\Http\Controller;

use App\Bundles\BecaModel\Dao\Clients;
use Sebk\SmallOrmSwoft\Factory\Dao;
use Swoft\Co;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Bean\Annotation\Mapping\Inject;

use App\Http\Middleware\JsonMiddleware;
use Swoft\Http\Server\Annotation\Mapping\Middleware;
use Swoft\Http\Server\Annotation\Mapping\Middlewares;
use Swoft\Http\Message\Request;


/**
 * Class SebkController
 *
 * @since 2.0
 *
 * @Controller("sebk")
 * @Middlewares({
 *     @Middleware (JsonMiddleware::class)
 * })
 */
class SebkController
{
    /**
     * @Inject()
     *
     * @var Dao
     */
    private $daoFactory;

    /**
     * @RequestMapping("save")
     * @Middleware (JsonMiddleware::class)
     *
     * @param Request $request
     * @return array
     */
    public function save(Request $request): array
    {
        // Get DAO
        $dao = $this->daoFactory->get('TestModel', 'TestSku');

        // Create model fom POST data
        $model = $dao->makeModelFormStdClass(json_decode($request->getRawBody()));

        // Validate
        if ($model->getValidator()->validate()) {
            // And persist
            $model->persist();
        } else {
            // Validation failed, send validator messages as response
            throw new \Exception(json_encode($model->getValidator()->getMessage()));
        }
        
        return $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 declare(strict_types=1);

namespace App\Http\Controller;

use Sebk\SmallOrmSwoft\Factory\Dao;
use Swoft\Co;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Bean\Annotation\Mapping\Inject;

use App\Http\Middleware\JsonMiddleware;
use Swoft\Http\Server\Annotation\Mapping\Middleware;
use Swoft\Http\Server\Annotation\Mapping\Middlewares;
use Swoft\Http\Message\Request;


/**
 * Class SebkController
 *
 * @since 2.0
 *
 * @Controller("sebk")
 * @Middlewares({
 *     @Middleware (JsonMiddleware::class)
 * })
 */
class SebkController
{
    /**
     * @Inject()
     *
     * @var Dao
     */
    private $daoFactory;

    /**
     * @RequestMapping("save")
     * @Middleware (JsonMiddleware::class)
     *
     * @param Request $request
     * @return array
     */
    public function save(Request $request): array
    {
        // Get default connection
        $connection = bean('sebk_small_orm_connections')->get();
        
        // Starting transaction
        $connection->startTransaction();

        // Get DAO
        $dao = $this->daoFactory->get('TestModel', 'TestSku');

        // Create model fom POST data
        $model = $dao->makeModelFormStdClass(json_decode($request->getRawBody()));

        // Validate
        if ($model->getValidator()->validate()) {
            try {
                // And persist
                $model->persist();
                $connection->commit();
            } catch (\Exception $e) {
                $connection->rollback();
            }
        } else {
            // Validation failed, send validator messages as response
            throw new \Exception(json_encode($model->getValidator()->getMessage()));
        }

        return $model;
    }
}

Use async requests with coroutine

The 'swoft-mysql' connector allow you to use coroutines to request database asynchronously.

Here is an example with multi method :

    /**
     * @RequestMapping("testAsync")
     * @Middleware (JsonMiddleware::class)
     *
     * @param Request $request
     * @return array
     */
    public function testAsync(Request $request): array
    {
        $num = $request->get("num");

        /** @var Clients $clientDao */
        $dao = $this->daoFactory->get('TestModel', 'TestSku');


        $requests = [];
        for ($i = 0; $i < $num; $i++) {
            $requests[] = function () use ($dao) {
                try {
                    return $dao->getFirsts(1);;
                } catch (\Exception $e) {
                    return [$e->getMessage()];
                }
            };
        }

        $result = [];
        foreach (Co::multi($requests) as $part) {
            $result = array_merge($result, $part);
        }

        return $result;
    }

Automatic CRUD generation

Small-orm-swoft come with a simple command to generate automatically a CRUD controller on a model.

Juste use this command :

$ bin/swoft sebk:small-orm:generate:crud --bundle OrderBundle --model Customer

It will generate a CustomerController.php in your Controller folder (see 'crudBasePath' in configuration section) :

<?php
namespace App\Http\Controller;

use App\Model\OrderBundle\Model\Customer;
use Sebk\SmallOrmCore\Dao\DaoEmptyException;
use Sebk\SmallOrmForms\Form\FormModel;
use Sebk\SmallOrmSwoft\Traits\Injection\DaoFactory;
use Swoft\Http\Message\Request;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\Http\Server\Annotation\Mapping\RequestMethod;
use Swoole\Http\Status;

/**
 * @Controller("customer")
 */
class CustomerController
{
    use DaoFactory;

    /**
     * Create form for Customer
     * @return FormModel
     * @throws \Sebk\SmallOrmForms\Form\FieldException
     * @throws \Sebk\SmallOrmForms\Type\TypeNotFoundException
     */
    protected function createForm(): FormModel
    {
        return (new FormModel())
            ->buildFromDao($this->daoFactory->get('OrderBundle', 'Customer'))
        ;
    }

    /**
     * @RequestMapping ("{id}", method={RequestMethod::GET})
     * @param int $id
     * @return \Swoft\Http\Message\Response
     */
    public function getCustomer(int $id)
    {
        // Load model
        try {
            /** @var Customer $model */
            $model = $this->daoFactory->get('OrderBundle', 'AvantageClient')->findOneBy(['idCustomer' => $id]);
        } catch (DaoEmptyException $e) {
            // Not found
            return JsonResponse('')
                ->withStatus(Status::NOT_FOUND)
            ;
        }

        return JsonResponse($model);
    }
    /**
     * @RequestMapping ("", method={RequestMethod::POST})
     * @param Request $request
     * @return \Swoft\Http\Message\Response
     * @throws \Exception
     */
    public function createCustomer(Request $request)
    {
        // Form validation
        $form = $this->createForm()
            ->fillFromArray(json_decode($request->getParsedBody(), true));
        $messages = $form->validate();
        if (count($messages) > 0) {
            // Validation failed
            return JsonResponse($messages)
                ->withStatus(Status::BAD_REQUEST);
        }

        // Persist
        $model = $form->fillModel()->persist();

        return JsonResponse($model);
    }
    /**
     * @RequestMapping ("{id}", method={RequestMethod::POST})
     * @param int $id
     * @param Request $request
     * @return \Swoft\Http\Message\Response
     * @throws DaoEmptyException
     * @throws \Sebk\SmallOrmForms\Form\FieldException
     * @throws \Sebk\SmallOrmForms\Form\FieldNotFoundException
     * @throws \Sebk\SmallOrmForms\Type\TypeNotFoundException
     */
    public function patchCustomer(int $id, Request $request)
    {
        // Load model
        try {
            /** @var Customer $model */
            $model = $this->daoFactory->get('OrderBundle', 'Customer')->findOneBy(['idCustomer' => $id]);
        } catch (DaoEmptyException $e) {
            // Not found
            return JsonResponse('')
                ->withStatus(Status::NOT_FOUND)
            ;
        }

        // Init data
        $data = json_decode($request->getParsedBody(), true);
        if (isset($data['idCustomer'])) {
            unset($data['idCustomer']);
        }

        // Form validation
        $form = $this->createForm()
            ->fillFromModel($model)
            ->fillFromArray($data)
        ;
        $messages = $form->validate();
        $model = $form->fillModel();

        if (count($messages) > 0) {
            // Validation failed
            return JsonResponse($messages)
                ->withStatus(Status::BAD_REQUEST)
            ;
        }

        // Persist
        $model->persist();

        return JsonResponse($model);
    }
}

It's resulting by routes creation :

  • GET '/customer/1' : get customer id 1 as json
  • POST '/customer' : create a customer from json body of request
  • POST '/customer/1' : update customer id 1 from json data in the request body

Note : you must require sebk/swoft-json-response and sebk/small-orm-forms in order to use this command :

$ composer require sebk/swoft-json-response
$ composer require sebk/small-orm-forms

Configuration

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

return [
    'bundlesBasePath' => __DIR__ . '/../app/Bundles/',
    'crudBasePath' => __DIR__ . '/../app/Http/Controller/',
    'connections' => [
        'default' => [
            'type' => 'swoft-mysql',
            'host' => 'database',
            'database' => 'db',
            'encoding' => 'utf8',
            'user' => 'root',
            'password' => 'dev',
            'tryCreateDatabase' => false,
        ],
    ],
    'bundles' => [
        'TestModel' => [
            'connections' => [
                'default' => [
                    'dao_namespace' => 'App\Bundles\TestModel\Dao',
                    'model_namespace' => 'App\Bundles\TestModel\Model',
                    'validator_namespace' => 'App\Bundles\TestModel\Validator',
                ],
            ],
        ],
    ],
];

How to get it

The source code is available on github : https://github.com/sebk69/small-orm-swoft

The core package (small-orm-core) source code is available on github : https://github.com/sebk69/small-orm-core

To use it :

Require the core package and swoft package with composer:

composer require sebk/small-orm-core
composer require sebk/small-orm-swoft


Author

Sébastien Kus
Web developer at La Bécanerie