byjg/micro-orm

A micro framework for create a very simple decoupled ORM (sqlite, mysql, postgres, sqlserver)

4.9.1 2024-01-05 20:13 UTC

This package is auto-updated.

Last update: 2024-04-14 22:08:47 UTC


README

Build Status Opensource ByJG GitHub source GitHub license GitHub release

A micro framework for create a very simple decoupled ORM. This library intended to be very small and very simple to use;

Key Features:

  • Can be used with any DTO, Entity, Model or whatever class with public properties or with getter and setter
  • The repository support a variety of datasources: MySql, Sqlite, Postgres, MySQL, Oracle (see byjg/anydataset)
  • A class Mapper is used for mapping the Entity and the repository
  • Small and simple to use

Architecture

These are the key components:

┌──────────────────────────┐
│ Repository               │              ┌─────────────────────┐
│                          │         ┌────│        Model        │
│                          │         │    └─────────────────────┘
│          ┌───────────────┴─────┐   │               │
│          │       Mapper        │───┤               │
│          └───────────────┬─────┘   │               │
│                     │    │         │    ┌─────────────────────┐
│                     │    │         └────│    FieldMapping     │
│                     │    │              └─────────────────────┘
│                     │    │
│          ┌───────────────┴─────┐
│          │        Query        │
│          └───────────────┬─────┘
│                          │
│          ┌───────────────┴─────┐
│          │  DbDriverInterface  │───────────────┐
│          └───────────────┬─────┘               │
│                          │                     │
└──────────────────────────┘                .─────────.
                                           │           │
                                           │`─────────'│
                                           │           │
                                           │    DB     │
                                           │           │
                                           │           │
                                            `─────────'
  • Model is a get/set class to retrieve or save the data into the database
  • Mapper will create the definitions to map the Model into the Database.
  • Query will use the Mapper to prepare the query to the database based on DbDriverInterface
  • DbDriverIntarce is the implementation to the Database connection.
  • Repository put all this together

Examples

For the examples below we will use the class 'Users';

<?php
class Users
{
    public $id;
    public $name;
    public $createdate;
}

First of all will create a Table Mapping class:

<?php
// Creating the mapping
$mapper = new \ByJG\MicroOrm\Mapper(
    Users::class,   // The full qualified name of the class
    'users',        // The table that represents this entity
    'id'            // The primary key field
);

// Optionally you can define table mappings between the propoerties
// and the database fields;
// The example below will map the property 'createdate' to the database field 'created';
$mapper->addFieldMapping(FieldMap::create('createdate')->withFieldName('created'));

Then you need to create the dataset object and the repository:

<?php
$dataset = \ByJG\AnyDataset\Db\Factory::getDbRelationalInstance('mysql://user:password@server/schema');

$repository = new \ByJG\MicroOrm\Repository($dataset, $mapper);

Some examples with the repository:

<?php

// Get a single data from your ID
$users = $repository->get(10);

// Persist the entity into the database:
// Will INSERT if does not exists, and UPDATE if exists
$users->name = "New name";
$repository->save($users);

Update Constraints

You can define a constraint to update a record. If the constraint is not satisfied the update will not be performed.

<?php
$updateConstraint = \ByJG\MicroOrm\UpdateConstraint()::instance()
    ->withAllowOnlyNewValuesForFields('name');

$users->name = "New name";
$repository->save($users, $updateConstraint);

Advanced uses

Get a collection using the query object:

<?php
$query = \ByJG\MicroOrm\Query::getInstance()
    ->table('users')
    ->fields(['id', 'name'])
    ->where('name like :part', ['part' => 'A%']);

// Will return a collection o 'Users'
$collection = $repository->getByQuery($query);

Returning multiples entities with a query:

<?php
$query = \ByJG\MicroOrm\Query::getInstance()
    ->table('order')
    ->join('item', 'order.id = item.orderid')
    ->where('name like :part', ['part' => 'A%']);

// Will return a collection of Orders and Items:
// $collection = [
//     [ $order, $item ],
//     [ $order, $item ],
//     ...
// ];
$collection = $orderRepository->getByQuery(
    $query,
    [
        $itemRepository->getMapper()
    ]
);

Using FieldAlias

Field alias is an alternate name for a field. This is usefull for disambiguation on join and leftjoin queries. Imagine in the example above if both tables ITEM and ORDER have the same field called 'ID'.

In that scenario, the value of ID will be overriden. The solution is use the FieldAlias like below:

<?php
// Create the Mapper and the proper fieldAlias
$orderMapper  = new \ByJG\MicroOrm\Mapper(...);
$orderMapper->addFieldMapping(FieldMapping::create('id')->withFieldAlias('orderid'));
$itemMapper  = new \ByJG\MicroOrm\Mapper(...);
$itemMapper->addFieldMappping(FieldMapping::create('id')->withFieldAlias('itemid'));

$query = \ByJG\MicroOrm\Query::getInstance()
    ->field('order.id', 'orderid')
    ->field('item.id', 'itemid')
        /* Other fields here */
    ->table('order')
    ->join('item', 'order.id = item.orderid')
    ->where('name like :part', ['part' => 'A%']);

// Will return a collection of Orders and Items:
// $collection = [
//     [ $order, $item ],
//     [ $order, $item ],
//     ...
// ];
$collection = $orderRepository->getByQuery(
    $query,
    [
        $itemRepository->getMapper()
    ]
);

You can also add a MAPPER as a Field. In that case the MAPPER will create the field and the correct aliases.

<?php
$query = \ByJG\MicroOrm\Query::getInstance()
    ->fields([
        $orderRepository->getMapper(),
        $itemRepository->getMapper,
    ]);

Tables without auto increments fields

<?php
// Creating the mapping
$mapper = new \ByJG\MicroOrm\Mapper(
    Users::class,   // The full qualified name of the class
    'users',        // The table that represents this entity
    'id',            // The primary key field
    function () {
        // calculate and return the unique ID
    }
);

Applying functions for Select and Update

<?php
// Creating the mapping
$mapper = new \ByJG\MicroOrm\Mapper(...);

$fieldMap = FieldMap::create('propertyname') // The property name of the entity class
    // The field name of the table. if not defined will use the property name.
    ->withFieldName('fieldname')
    // The field alias of the field in the table. if not defined will use the field name.
    ->withFieldAlias('alias')
    // Returns the pre-processed value before UPDATE/INSERT the $field name
    // If the function returns NULL this field will not be included in the UPDATE/INSERT
    ->withUpdateFunction(function ($field, $instance) {
        return $field;
    })
    // Returns the field value with a post-processed value of $field AFTER query from DB
    ->withSelectFunction(function ($field, $instance) {
        return $field;
    })

$mapper->addFieldMapping($fieldMap);

Observers

You can add observers to the repository. The observer will be called after the insert, update or delete a record in the DB.

flowchart TD
    A[MyRepository] --> |1. addObserver| B[Subject]
    C[ObserverdRepository] --> |2. Notify Update| B
    B --> |3. Execute Callback| A
<?php
// This observer will be called after insert, update or delete a record on the table 'triggerTable' 
$myRepository->addObserver(new class($this->infoMapper->getTable()) implements ObserverProcessorInterface {
    private $table;

    public function __construct($table)
    {
        $this->table = $table;
    }

    public function process(ObserverData $observerData)
    {
        // Do something here
    }

    public function getObserverdTable(): string
    {
        return $this->table;
    }
});

The ObserverData class contains the following properties:

  • getTable(): The table name that was affected
  • getEvent(): The event that was triggered. Can be 'insert', 'update' or 'delete'
  • getData(): The data that was inserted or updated. It is null in case of delete.
  • getOldData(): The data before update. In case of insert comes null, and in case of delete comes with the param filters.
  • getRepository(): The repository is listening to the event (the same as $myRepository)

Note: The observer will not be called if the insert, update or delete is called using the DBDriver object.

Using With Recursive SQL Command

<?php
$recursive = \ByJG\MicroOrm\Recursive::getInstance('test')
    ->field('start', 1, 'start + 10')
    ->field('end', 120, "end - 10")
    ->where('start < 100')
;

$query = \ByJG\MicroOrm\Query::getInstance()
    ->withRecursive($recursive)
    ->fields(['start', 'end']);

/*
This will produce the following SQL:

WITH RECURSIVE test(start, end) AS (
    SELECT 1 as start, 120 as end
    UNION ALL SELECT start + 10, end - 10 FROM test WHERE start < 100
) SELECT start, end FROM test
*/

Pre-defined closures for field map

Mapper::defaultClosure($value, $instance)

Defines the basic behavior for select and update fields;

Mapper::doNotUpdateClosure($value, $instance)

If set in the update field map will make the field not updatable by the micro-orm. It is usefull for fields that are pre-defined like 'Primary Key'; timestamp fields based on the update and the creation; and others

Before insert and update functions

You can also set closure to be applied before insert or update a record. In this case will set in the Repository:

<?php
Repository::setBeforeInsert(function ($instance) {
    return $instance;
});

Repository::setBeforeUpdate(function ($instance) {
    return $instance;
});

TransactionManager object

It allows you to create a single database transaction with multiple repositories. If any of the repositories fails the transaction will be rolled back for all repositories. When you commit the transaction all repositories will be commited.

<?php
$repo1 = new Repository(...);
$repo2 = new Repository(...);

// Create the TransactionManager
$transactionManager = new TransactionManager();
$transactionManager->addRepository($repo1);
$transactionManager->addRepository($repo2);

// Start the transaction
$transactionManager->beginTransaction();

//
// Do some Repository operations with the repo;
// ...

// commit (or rollback all transactions)
$transactionManager->commitTransaction();

Install

Just type:

composer require "byjg/micro-orm"

Running Tests

./vendor/bin/phpunit

Related Projects

Dependencies

flowchart TD
    byjg/micro-orm --> byjg/anydataset-db
    byjg/micro-orm --> ext-json

Open source ByJG