Igni storage is minimalistic mapping/hydration framework with support for PDO and MongoDB databases with cross database access.
<?php declare(strict_types=1);
use Igni\Storage\Driver\Pdo\Connection;
use Igni\Storage\Storage;
use Igni\Storage\Driver\ConnectionManager;
// Define connection:
ConnectionManager::registerDefault(new Connection('sqlite:/' . __DIR__ . '/db.db'));
// Initialize storage:
$storage = new Storage();
$artist = $storage->get(Artist::class, 1);
// Update artist's name
$artist->name = 'John Lennon';
// Save changes in memory
$storage->persist($artist);
// Commit changes to database
$storage->commit();
Just pass your query to the driver, you are no longer limited to custom query builders api, or complex setup and hacks to force library to work with your input.
There is one page documentation which you can grasp in an hour and many examples that are working straight away without complex configuration.
Mongo, pgsql, mysql, sqlite - you can use all of them together. If this is not sufficient you can write custom driver to support database of your choice.
Allows you to store complex data in your database
It does not matter if you use mongo with sqlite or mysql or any other database, you can keep references to entities stored in different types of databases with ease.
Collection and LazyCollection classes provides interface that supports declarative programming.
-
= PHP 7.1
- PDO for mysql, sqlite and/or pgsql support
- MongoDB extension for mongo support
composer install igniphp/storage
Igni strongly bases on repository and unit of work patterns. This two patterns are intended to create an abstraction layer between the data access layer and the business logic layer of your application.
The facilitation that is created by UoW makes track of changes and automated unit testing to be achieved in much simpler manner.
Shortly saying UoW maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems (source)._
Entity Storage is responsible for providing UoW implementation.
Repository is a central place where data is stored and maintained. Igni provides basic implementation per each of the supported drivers:
<?php
use Igni\Storage\Driver\Pdo\Connection;
use Igni\Storage\Driver\Pdo\ConnectionOptions;
new Connection('mysql:host=localhost', new ConnectionOptions(
$database = 'test',
$username = 'root',
$password = 'password'
));
<?php
use Igni\Storage\Driver\Pdo\Connection;
$connection = new Connection('sqlite:/path/to/database');
<?php
use Igni\Storage\Driver\MongoDB\Connection;
use Igni\Storage\Driver\MongoDb\ConnectionOptions;
$connection = new Connection('localhost', new ConnectionOptions(
$database = 'test',
$username = 'test',
$password = 'password'
));
Connection manager is static class used for registering and obtaining active connections by repositories. If you would like your repositories to automatically retrieve a connection you have to register one in the connection manager.
<?php
use Igni\Storage\Driver\ConnectionManager;
ConnectionManager::register($name = 'my_connection', $connection);
<?php
use Igni\Storage\Driver\ConnectionManager;
ConnectionManager::has($name = 'my_connection');// return true
<?php
use Igni\Storage\Driver\ConnectionManager;
ConnectionManager::get($name = 'my_connection');// return true
<?php
use Igni\Storage\Driver\ConnectionManager;
// Releases and closes all registered connections
ConnectionManager::release();
Mapping tells how data that is stored in database should be reflected in your code.
Library provides following tools to map data:
- Repositories (manages entities and provides access to your entities)
- Cursors (used to process and execute queries in the database)
- Collections (abstraction layer around cursors)
- Hydrators (used to map data from and to database)
- Entities (unit of data, can be single person, place or thing)
Repository is a central place where data is stored and maintained. Repository must implement Repository interface or extend one of the provided repository classes, depending which database you are using:
Repositories have to be defined and registered in order to be recognized by unit of work.
<?php declare(strict_types=1);
use Igni\Storage\Driver\Pdo;
use Igni\Storage\Driver\MongoDB;
// Use pdo repository
class TrackRepository extends Pdo\Repository
{
public static function getEntityClass(): string
{
return Track::class;
}
}
// Use mongodb repository
class PlaylistRepository extends MongoDB\Repository
{
public static function getEntityClass(): string
{
return Playlist::class;
}
}
<?php declare(strict_types=1);
use Igni\Storage\Storage;
// Initialize storage:
$storage = new Storage();
// Add repository
$storage->addRepository(new TrackRepository($storage->getEntityManager()));
An entity is an object that exists. It can perform various actions and has its own identity. An entity can be a single thing, person, place, or object. Entity defines attributes, which keeps information about what entity needs in order to live.
Entity must implement \Igni\Storage\Storable
interface in order to be stored, updated or deleted.
The interface requires you to define getId
method.
The simplest entity may look like this:
<?php
use Igni\Storage\Storable;
use Igni\Storage\Id;
use Igni\Storage\Id\Uuid;
class SongEntity implements Storable
{
private $id;
public function __construct()
{
$this->id = new Uuid();
}
public function getId(): Id
{
return $this->id;
}
}
This entity cannot be stored yet. What is missing here is:
- the information where entity should be persisted
- which property keeps entity's identity
This and other meta information can be injected to the entity with annotations. Annotation is a note by way of explanation/comment
added to a code. In php world it is kept in doc block comment prefixed by @
.
Following example stores song in table/collection named songs
with identity set on id
property.
<?php
use Igni\Storage\Storable;
use Igni\Storage\Id;
use Igni\Storage\Id\Uuid;
use Igni\Storage\Mapping\Annotation as Storage;
/**
* @Storage\Entity(source="albums", connection="default")
*/
class SongEntity implements Storable
{
/**
* @var Uuid
* @Storage\Types\Id()
*/
private $id;
public function __construct()
{
$this->id = new Uuid();
}
public function getId(): Id
{
return $this->id;
}
}
The above entity can be stored, retrieved and deleted but it contains no viable data like: title, artist, album, etc. Altering more data in the entity can be achieved by creating more properties and annotating them with desired type annotation.
Used to register an entity within storage framework
source
(required) generally speaking this is name of place where entity is kept in your database (collection, table, etc.)
hydrator
class name of custom hydrator that should be used during retrieve and persist process
connection
specify the connection name that should be used by entity's repository
Types are used to tell library how properties should be treated when data is retrieved and/or stored.
Igni contains 9 built-in types that you can use straight away and can be found in Igni\Storage\Mapping\Strategy
namespace.
Each of the built-in type also have corresponding annotation that can be found in Igni\Storage\Mapping\Annotations\Types
namespace.
Used to map datetime and date data types.
name
keeps equivalent key name stored in database
format
string representation of a valid format that is being used to store the value
timezone
string representation of any valid timezone that is being used to store the value
immutable
tells whether the value should be instantiated as \DateTimeImmutable
or \DateTime
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
class Example implements Igni\Storage\Storable
{
/**
* @Igni\Storage\Mapping\Annotation\Property\Date(format="Ymd", immutable=true, timezone="UTC")
*/
private $value;
public function getId(): Igni\Storage\Id
{
//...
}
}
Decimals are safe way to deal with fragile numerical data like money. bcmath extension is required in order to use decimal values.
name
keeps equivalent key name stored in database
scale
is the number of digits to the right of the decimal point in a number
precision
is the number of digits in a number
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\Entity(source="examples") */
class Example implements Igni\Storage\Storable
{
/**
* For example we can store the number 12.45 that has a precision of 4 and a scale of 2.
* @Igni\Storage\Mapping\Annotation\Property\DecimalNumber(scale=2, precision=4)
*/
private $value;
public function getId(): Igni\Storage\Id
{
//...
}
}
Embed is an object that is not entity itself but it is composed into the entity. Embeds can be stored in the database as json or serialized php array.
class
(required) contains information about the type of embed object
name
keeps equivalent key name stored in database
storeAs
keeps information how data should be stored in the column/property. Can be one of the following values:
- plain
- json
- serialized
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\EmbeddedEntity() */
class Address
{
/** @var Igni\Storage\Mapping\Annotation\Property\Text() */
private $street;
/** @var Igni\Storage\Mapping\Annotation\Property\Text() */
private $postalCode;
/** @var Igni\Storage\Mapping\Annotation\Property\Text() */
private $city;
}
/** @Igni\Storage\Mapping\Annotation\Entity(source="users") */
class User implements Igni\Storage\Storable
{
/** @var Igni\Storage\Mapping\Annotation\Property\Embed(Address::class, storeAs="json") */
private $address;
public function getId(): Igni\Storage\Id
{
//...
}
}
Note: Storing embeds as json in SQL databases can be really usefull, databases like MySQL or PgSQL have good support
for JSON datatypes.
Enums should be always used when variable can be one out of small set of possible values. It can be used to save storage space, add additional checks in your code, etc.
values
(required) can be either class that implements Igni\Storage\Enum
interface or array of values
name
keeps equivalent key name stored in database
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
class AudioType implements \Igni\Storage\Enum
{
const MPEG = 0;
const AAC = 1;
const MPEG_4 = 2;
private $value;
public function __construct($value)
{
$this->value = (int) $value;
if (!in_array($this->value, [0, 1, 2])) {
throw new \InvalidArgumentException('Invalid audio type');
}
}
public function getValue(): int
{
return $this->value;
}
}
/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
/** @var Igni\Storage\Mapping\Annotation\Property\Enum(AudioType::class) */
private $audioTypeEnumClass; // This will be instance of AudioType class
/** @var Igni\Storage\Mapping\Annotation\Property\Enum({"MPEG", "AAC", "MPEG-4"}) */
private $audioTypeList; // This can be one of the following strings: "MPEG", "AAC", "MPEG-4", but persisted as integer.
public function getId(): Igni\Storage\Id
{
//...
}
}
Maps float numbers.
name
keeps equivalent key name stored in database
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
/** @var Igni\Storage\Mapping\Annotation\Property\Float(name="audio_length") */
private $length;
public function getId(): Igni\Storage\Id
{
//...
}
}
Id is value object that is used to update, remove and retrieve documents by default repository classes. Once id is set on an object it should not be changed during the runtime.
If no class
attribute is specified id by default becomes instance of Igni\Storage\Id\GenericId
.
You can map id to your custom class that implements Igni\Storage\Id
interface.
Igni provides two default implementations for id value object:
Igni\Storage\Id\GenericId
Igni\Storage\Id\Uuid
Igni\Storage\Id\GenericId
can be any value, it accepts everything by default and it is not recommended to use it unless
you have no other option.
Igni\Storage\Id\Uuid
any value passed to the constructor of this class must be valid uuid number. Uuid is kept as 21-22
long varchar value to save the storage space.
name
keeps equivalent key name stored in database
class
once this is set id becomes instance of the given class
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
/** @var Igni\Storage\Mapping\Annotation\Property\Id(class=Igni\Storage|Id\Uuid::class) */
private $id;
public function getId(): Igni\Storage\Id
{
return $this->id;
}
}
The following example shows how to auto-generate ids for your entity.
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
use Igni\Storage\Id\AutoGenerateId;
}
Maps integer numbers.
name
keeps equivalent key name stored in database
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
/** @var Igni\Storage\Mapping\Annotation\Property\IntegerNumber() */
private $length;
public function getId(): Igni\Storage\Id
{
//...
}
}
name
keeps equivalent key name stored in database
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
/** @var Igni\Storage\Mapping\Annotation\Property\Text() */
private $lyrics;
public function getId(): Igni\Storage\Id
{
//...
}
}
References are properties that stores ids to other entities in your data layer. Storage framework resolves them automatically on hydration phase.
target
(required) FQCN of the entity that is stored as reference in the property
name
keeps equivalent key name stored in database
readonly
property marked as readonly is ignored during persistence operations
<?php declare(strict_types=1);
/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
/** @var Igni\Storage\Mapping\Annotation\Property\Reference(target=Album::class) */
private $album;
public function getId(): Igni\Storage\Id
{
//...
}
public function getAlbum(): Album
{
return $this->album;
}
}
If entity has to store collection of references it is recommended to create custom hydrator.
Auto-resolving complex schema is memory and cpu consuming and in most cases not sufficient enough. At the time like this it is good to have set of tools that will support you in building application layer where you have total control what is happening on your database layer. Storage framework was build to provide this kind set of tools, one of them is possibility to define and use custom hydrators to help you out with reflecting database schema in your application code.
Custom hydrator is a decorator for hydrator generated by Igni\Storage\Hydration\HydratorFactory
and must implement \Igni\Storage\Hydration\ObjectHydrator
interface.
The following code is the simplest implementation of custom hydrator:
<?php
class CustomTrackHydrator implements Igni\Storage\Hydration\ObjectHydrator
{
private $baseHydrator;
public function __construct(Igni\Storage\Hydration\GenericHydrator $baseHydrator)
{
$this->baseHydrator = $baseHydrator;
}
public function hydrate(array $data)
{
$entity = $this->baseHydrator->hydrate($data);
// Modify entity to your needs
return $entity;
}
public function extract($entity): array
{
$extracted = $this->baseHydrator->extract($entity);
// Modify the data before storing it in database.
return $extracted;
}
}
Storage framework can recognize custom-defined hydrator once it is set in the @Entity
annotation.
With custom hydrators you can define your own many-to-many and one-to-many relation handling and more.
<?php
/**
* @Igni\Storage\Mapping\Annotation\Entity(source="tracks", hydrator=CustomTrackHydrator::class)
*/
class TrackEntity implements Igni\Storage\Storable
{
public function getId(): Igni\Storage\Id
{
// ...
}
}
For full example please visit examples directory.
Storage provides useful set of daily-basis types like: int, decimal float or reference. If you find you lack some type that will fulfill your needs there is easy way to define your own custom data-type.
There are two steps required to create custom data-type:
- Create class responsible for data mapping (from and to database)
- Registering defined type
Mapping strategy class must implement Igni\Storage\Mapping\MappingStrategy
interface:
<?php
final class MyType implements Igni\Storage\Mapping\MappingStrategy
{
public static function hydrate(&$value)
{
// Here format data that will be used in the code-land
}
public static function extract(&$value)
{
// Here format data that will be persisted to database
}
}
For full example please visit examples directory.
Collection is a container that groups multiple elements into a single unit. Storage framework contains collection implementation that allows to easier represent or manipulate data that are provided by cursor or any other Iterable instance.
Storage framework's collection supports declarative programming, it has support for map/reduce operations.
Collection can be instantiated with any iterable including cursor.
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
// From iterable
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT *FROM artists'));
// From list of items
$numbers = Collection::fromList(1, 2, 3);
<?php
use Igni\Storage\Mapping\Collection\Collection;
$collection = new Collection();
$withNewItem = $collection->add(1);
$withMultipleItems = $collection->addMany(1, 2, 3);
<?php
use Igni\Storage\Mapping\Collection\Collection;
$collection = new Collection([1, 2, 3, 4]);
$collectionWithoutItem = $collection->remove(2);
$collectionWithoutManyItems = $collection->removeMany(1, 4);
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT *FROM artists'));
// Imperative approach.
$mappedData = new Collection();
foreach ($collection as $item) {
$item['age'] = 20;
$mappedData = $mappedData->add($item);
}
// Declarative approach
$mappedData = $collection->map(function ($item) {
$item['age'] = 20;
return $item;
});
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT *FROM artists'));
// Sort by age
$sorted = $collection->sort(function(array $current, array $next) {
return $current['age'] <=> $next['age'];
});
// Reverse
$reversed = $sorted->reverse();
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT name, age FROM artists'));
if ($collection->contains(['name' => 'Bob', 'age' => 20])) {
// There is Bob in the collection
}
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT *FROM artists'));
// Age greater than 50
$elders = $collection->where(function(array $artist) {
return $artist['age'] > 50;
});
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT *FROM artists'));
if ($collection->any(function($artist) { return $artist['age'] > 70; })) {
// There is at least one artist who is over 70 yo
}
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT *FROM artists'));
if ($collection->every(function($artist) { return $artist['age'] > 2; })) {
// All artists are above 2 yo
}
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\Collection;
$connection = ConnectionManager::getDefault();
$collection = new Collection($connection->execute('SELECT *FROM artists'));
$totalAge = $collection->reduce(
function(int $total, array $artist) {
return $total + $artist['age'];
},
$initialValue = 0
);
Lazy collection are immutable lazy bastards, they do nothing all the day but iterate through cursor in the way where item is not fetched from database until it is really needed (such lazy, wow!). (If you reached this point of documentation take my congratulations :D)
Lazy collection was specially made to work with cursors so it accepts only cursors:
<?php
use Igni\Storage\Driver\ConnectionManager;
use Igni\Storage\Mapping\Collection\LazyCollection;
$connection = ConnectionManager::getDefault();
$lazyBastard = new LazyCollection($connection->execute('SELECT *FROM artists'));
// Iterating
foreach ($lazyBastard as $item) {
// Do something here
}
// You have changed your mind and get fed with laziness- no probs:
$nonLazy = $lazyBastard->toCollection();