igniphp/storage

支持多数据库的最简实体框架。

0.6.0 2018-09-27 06:35 UTC

This package is not auto-updated.

Last update: 2024-09-20 20:36:38 UTC


README

Build Status Scrutinizer Code Quality Code Coverage

Igni Storage

Igni storage 是一个最简的映射/活化框架,支持 PDO 和 MongoDB 数据库,具有跨数据库访问功能。

简介

<?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();

目录

特性

支持原生查询

只需将您的查询传递给驱动程序,您不再受自定义查询构建器 API 或复杂设置和黑客攻击的限制,以强制库与您的输入一起工作。

学习曲线小

有一个页面文档,您可以在一个小时内掌握,还有许多可以直接使用而无需复杂配置的示例。

支持多种数据库类型

Mongo、pgsql、mysql、sqlite - 您可以使用所有这些。如果这还不够,您可以编写自定义驱动程序以支持您选择的数据库。

嵌入实体

允许您在数据库中存储复杂数据

跨数据库引用

无论您使用 mongo、sqlite、mysql 还是任何其他数据库,您都可以轻松地保持对存储在不同类型数据库中的实体的引用。

支持声明式编程

Collection 和 LazyCollection 类提供支持声明式编程的接口。

要求

  • = PHP 7.1

  • PDO 用于 mysql、sqlite 和/或 pgsql 支持
  • MongoDB 扩展用于 mongo 支持

安装

composer install igniphp/storage

基本概念

Igni 强烈基于仓库和工作单元模式。这两个模式旨在在您应用程序的数据访问层和业务逻辑层之间创建一个抽象层。

UoW 所创建的便利性使得跟踪更改和自动单元测试得以以更简单的方式进行。

工作单元

简而言之,UoW 维护一个受业务事务影响的对象列表,并协调更改的写入和并发问题的解决(来源)。

实体存储 负责提供 UoW 实现。

仓库

仓库是一个集中存储和维护数据的地方。Igni 为每个支持的驱动程序提供基本实现。

连接

Mysql/PgSQL

<?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'
));

Sqlite

<?php
use Igni\Storage\Driver\Pdo\Connection;

$connection = new Connection('sqlite:/path/to/database');

MongoDB

<?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'
));

连接管理器

连接管理器是一个静态类,用于通过仓库注册和获取活动连接。如果您希望您的仓库自动检索连接,则必须在连接管理器中注册一个。

注册连接

<?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();

映射

映射说明数据库中存储的数据应如何反映在您的代码中。

库提供了以下工具来映射数据

  • 仓库(管理实体并提供对您的实体的访问)
  • 游标(用于在数据库中处理和执行查询)
  • 集合(围绕游标提供的抽象层)
  • 数据注水器(用于将数据映射到和从数据库中)
  • 实体(数据单位,可以是单个人员、地点或事物)

仓库

仓库是一个存储和维护数据的中枢位置。仓库必须实现仓库接口或扩展提供的仓库类中的一个,具体取决于您使用的数据库

必须定义和注册仓库,以便工作单元能够识别它们。

定义仓库

<?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()));

实体

实体是一个存在的对象。它可以执行各种操作,并具有自己的身份。实体可以是一件单独的事物、人、地点或物体。实体定义属性,这些属性保存有关实体需要什么才能生存的信息。

定义实体

为了存储、更新或删除,实体必须实现\Igni\Storage\Storable接口。该接口要求您定义getId方法。

最简单的实体可能看起来像这样

<?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;
    }
}

此实体尚不能存储。这里缺少的是

  • 实体应该持久化的信息
  • 哪个属性保持实体的身份

这些和其他元信息可以通过注解注入到实体中。注解是一种通过说明/注释添加到代码中的注释。在PHP世界中,它以@为前缀的文档注释形式存在。

以下示例将歌曲存储在名为songs的表/集合中,身份设置为id属性。

<?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;
    }
}

上面的实体可以存储、检索和删除,但它不包含像:标题、艺术家、专辑等有效数据。通过创建更多属性并用所需类型注解来更改实体中的更多数据是可以实现的。

实体注解

用于在存储框架内注册实体

接受的属性

source (必需) 通俗地说,这是实体在您的数据库中(集合、表等)保存的地方的名称

hydrator 在检索和持久化过程中应使用的自定义注水器的类名

connection 指定实体仓库应使用的连接名称

类型

类型用于告诉库在检索和/或存储数据时应如何处理属性。Igni包含9个内置类型,您可以直接使用,并在Igni\Storage\Mapping\Strategy命名空间中找到。每个内置类型也有对应的注解,可以在Igni\Storage\Mapping\Annotations\Types命名空间中找到。

日期

用于映射日期和时间数据类型。

接受的属性

name 保存数据库中存储的等效键名

format 存储值时使用的有效格式的字符串表示形式

timezone 存储值时使用的任何有效时区的字符串表示形式

immutable 告知是否应将值实例化为\DateTimeImmutable\DateTime

readonly 标记为只读的属性在持久化操作期间被忽略

<?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 
    {
        //...
    }
}

十进制

十进制是处理像金钱这样的脆弱数值数据的安全方式。bcmath扩展是使用十进制值所必需的。

接受的属性

name 保存数据库中存储的等效键名

scale 是数字小数点右侧的数字位数

precision 是数字中的数字位数

readonly 标记为只读的属性在持久化操作期间被忽略

<?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 
    {
        //...
    }
}

嵌入

嵌入是一个不是实体本身的对象,但它是由实体组成的。嵌入可以以json或序列化PHP数组的形式存储在数据库中。

接受的属性

class (必需) 包含有关嵌入对象类型的信息

name 保存数据库中存储的等效键名

storeAs 保存有关数据应如何存储在列/属性中的信息。可以是以下值之一

  • plain
  • json
  • serialized

readonly 标记为只读的属性在持久化操作期间被忽略

<?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.

枚举

当变量可以是从一个小集合中选择的一个值时,应始终使用枚举。它可以用于节省存储空间,在代码中添加额外的检查等。

接受的属性

values (必需) 可以是实现了 Igni\Storage\Enum 接口的类,或者是值的数组。

name 保存数据库中存储的等效键名

readonly 标记为只读的属性在持久化操作期间被忽略

<?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 
    {
        //...
    }
}

浮点数

映射浮点数。

接受的属性

name 保存数据库中存储的等效键名

readonly 标记为只读的属性在持久化操作期间被忽略

<?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

Id 是默认的仓库类用来更新、删除和检索文档的值对象。一旦对象设置了 id,在运行时不应更改。

如果没有指定 class 属性,默认情况下 id 变为 Igni\Storage\Id\GenericId 的实例。您可以将 id 映射到实现了 Igni\Storage\Id 接口的自定义类。

Igni 提供了两个默认的 id 值对象实现。

  • Igni\Storage\Id\GenericId
  • Igni\Storage\Id\Uuid

Igni\Storage\Id\GenericId 可以是任何值,它默认接受所有值,不建议在没有其他选择的情况下使用。

Igni\Storage\Id\Uuid 传递给该类构造函数的任何值必须是有效的 uuid 数字。为了节省存储空间,uuid 保留为 21-22 位长的 varchar 值。

接受的属性

name 保存数据库中存储的等效键名

class 一旦设置,id 就变为指定类的实例。

<?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;
    }
}
自动生成的 id

以下示例显示了如何为实体自动生成 id。

<?php declare(strict_types=1);

/** @Igni\Storage\Mapping\Annotation\Entity(source="tracks") */
class Track implements Igni\Storage\Storable
{
    use Igni\Storage\Id\AutoGenerateId;
}

整数

映射整数。

接受的属性

name 保存数据库中存储的等效键名

readonly 标记为只读的属性在持久化操作期间被忽略

<?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 保存数据库中存储的等效键名

readonly 标记为只读的属性在持久化操作期间被忽略

<?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 
    {
        //...
    }
}

引用

引用是存储在数据层中其他实体 id 的属性。存储框架在加载数据时自动解析它们。

接受的属性

target (必需) 存储在属性中作为引用的实体的完全限定类名 (FQCN)

name 保存数据库中存储的等效键名

readonly 标记为只读的属性在持久化操作期间被忽略

<?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;
    }
}

如果实体必须存储引用的集合,建议创建自定义加载数据处理器。

与自定义活化器一起工作

自动解析复杂模式是内存和 CPU 消耗的,在大多数情况下不足以满足需求。在这种情况下,拥有一套工具来支持你在应用层完全控制数据库层是非常有用的。存储框架正是为此而构建的,其中之一是定义和使用自定义加载数据处理器来帮助您将数据库模式反映到应用代码中。

自定义加载数据处理器是 Igni\Storage\Hydration\HydratorFactory 生成的加载数据处理器的装饰器,并且必须实现 \Igni\Storage\Hydration\ObjectHydrator 接口。

以下代码是自定义加载数据处理器的最简单实现。

<?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;
    }
}

存储框架可以在 @Entity 注解中识别已设置的自定义定义的加载数据处理器。

使用自定义加载数据处理器,您可以定义自己的多对多和一对一关系处理等。

<?php

/**
 * @Igni\Storage\Mapping\Annotation\Entity(source="tracks", hydrator=CustomTrackHydrator::class)
 */
class TrackEntity implements Igni\Storage\Storable
{
    public function getId(): Igni\Storage\Id 
    {
        // ...
    }
}

有关完整示例,请访问 示例目录

与自定义类型一起工作

存储提供了诸如 int、decimal float 或引用等日常基础类型。如果您发现缺少满足您需求的某些类型,您可以轻松地定义自己的自定义数据类型。

创建自定义数据类型需要两个步骤。

  • 创建负责数据映射(从数据库到和从数据库)的类。
  • 注册定义的类型。

映射策略类必须实现 Igni\Storage\Mapping\MappingStrategy 接口。

<?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
    }
}

有关完整示例,请访问 示例目录

与集合一起工作

集合是一个将多个元素组合成一个单一单元的容器。存储框架包含集合实现,它允许更轻松地表示或操作由游标或其他可迭代实例提供的数据。

存储框架的集合支持声明式编程,它具有对 map/reduce 操作的支持。

创建新的集合实例。

集合可以用任何可迭代对象实例化,包括游标。

<?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
);

处理懒集合

懒集合是不可变的懒虫,它们整天无所事事,只是以某种方式遍历游标,直到真正需要时才从数据库中检索项目(如此懒惰,哇!)。(如果你已经看到这里,请接受我的祝贺 :D)

懒集合特别设计用于与游标一起使用,因此它只接受游标

<?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();
就这些了,朋友们!