phpixie/orm

PHPixie 的 ORM 库

3.19 2019-12-25 23:21 UTC

README

PHPixie ORM 库

Build Status Test Coverage Code Climate HHVM Status

Author Source Code Software License Total Downloads

初始化

$slice = new \PHPixie\Slice();
$database = new \PHPixie\Database($slice->arrayData(array(
    'default' => array(
        'driver' => 'pdo',
        'connection' => 'sqlite::memory:'
    )
)));
$orm = new \PHPixie\ORM($database, $slice->arrayData(array(
    // We will add some configuration later
)));

如果您使用 PHPixie 框架,ORM 组件已经为您设置好了。您可以通过 $frameworkBuilder->components()->orm() 访问它,并且可以在您包的 config/orm.php 文件中进行配置。

模型

模型由存储库、查询和实体组成,通常映射到关系数据库中的一个表或在 MongoDB 中的一个文档集合。

  • 实体 - 数据库中存储的单个项目,例如文章
  • 存储库 - 用于创建和保存实体
  • 查询 - 用于选择、更新或删除实体

您可以直接开始使用模型,无需任何配置,它将使用基于模型名称的默认设置。

$repository = $orm->repository('article');

$newArticle   = $repository->createEntity();
$articleQuery = $repository->query();

// shorthands
$newArticle   = $orm->createEntity('article');
$articleQuery = $orm->query('article');

配置

默认情况下,ORM 假设表名是模型名称的复数形式,主键名称为 'id'。对于 MongoDB 数据库,默认的 id 字段为 '_id'。您可以在配置文件中为特定模型覆盖这些设置

return array(
    'models' => array(
        'article' => array(
            'type'       => 'database',
            'connection' => 'default',
            'id'         => 'id'
        ),

        // you can also define embedded models
        // if you are using MongoDB,
        // more on that later
    )
);

实体

// Saving an entity
$article->title = 'Welcome';
$article->save();

// Getting a field with a default value
$article->getField('title', 'No Title');

// Getting a required field
// Will throw an Exception if it is not set
$article->getRequiredField('title');

// Convert to a simple PHP object
// Usefull for serializing
$object = $article->asObject();

// Deleting
$article->delete();

查询

ORM 查询与数据库查询共享许多语法,以下是一些查询示例

// Find article by name
$article = $orm->query('article')
    ->where('title', 'Welcome')
    ->findOne();

// Find by id
$article = $orm->query('article')
    ->in($id)
    ->findOne();

// Query by multiple ids
$articles = $orm->query('article')
    ->in($ids)
    ->findOne();

// Actually the in() method can be used
// to also include subqueries and entities.
//
// This will select articles with id '1',
// or the id same as $someArticle, and
// anything matched by $subQuery
$articles = $orm->query('article')
    ->in(array(
        1, $someArticle, $subQuery
    ));

// Multiple conditions
$articles = $orm->query('article')
    ->where('viewsTotal', '>', 2)
    ->or('viewsDone', '<', 5)
    ->find();

// Ordering
// Supports multiple fields
$articles
    ->orderAscendingBy('name')
    ->orderDescendingBy('id');

// Limit and offset
$articles
    ->limit(1)
    ->offset(2);

// It continues in the same way
// as in the Database component

$articles = $orm->query('article')
    ->where('name', 'Welcome')
    ->or(function($query) {
        $querty
            ->where('viewsTotal', '>', 2)
            ->or('viewsDone', '<', 5);
    })
    ->find();

// Alternative syntax for
// nested conditions
$articles = $orm->query('article')
    ->where('name', 'Welcome')
    ->startWhereConditionGroup('or')
        ->where('viewsTotal', '>', 2)
        ->or('viewsDone', '<', 5)
    ->endGroup()
    ->find();

如果使用 findOne,则查询将返回单个项目或 null。当使用 find 时,将返回一个加载器,可以按以下方式使用

// Iterate over it
// Note: this can be done only once
foreach($articles as $article) {
    // ...
}

// Convert into an array
// to allow multiple iterations
$articles = $articles->asArray();

// Convert it into plain objects,
// useful for serializing
$data = $articles->asArray(true);

// Convert it into associative array
$data = $articles->asArray(true, 'id');

如果您将要访问多个您正在选择的项目的关系,您应该预加载它们的关系以避免多次数据库查询。这称为 预加载

// To load relationships eagerly
// just pass their names to find() or findOne()
$articles = $query->find(array(
    'author',
    'tags'
));

// You can also preload nested relationships
// using the dot notation
$articles = $query->find(array(
    'author.friends',
));

查询也可用于更新和删除项目

// Count matched items
$count = $articleQuery->count();

// Delete matched items
$articleQuery->delete();

// Update a field in all matched items
$articleQuery
    ->update(array(
        'status' => 'published'
    ));

// Some more advanced updating
$articleQuery
    ->getUpdateBuilder()
        ->increment('views', 1)
        ->set('status', 'published')
        // Removing a field in MongoDB
        ->remove('isDraft')
        ->execute();

扩展模型

扩展 ORM 模型通常会导致开发人员将业务逻辑与数据库耦合,这使得它们难以测试和调试。在这些情况下,进行任何类型的测试都需要在数据库中插入一些示例测试数据。PHPixie ORM 通过允许类似装饰器的行为并使用包装器来解决这个问题。您不是扩展类,而是提供包装器,这些包装器可以用于包装 ORM 实体、查询和存储库,并为其添加功能。

class UserEntity extends \PHPixie\ORM\Wrappers\Type\Database\Entity
{
    // Get users full name
    public function fullName()
    {
        // You can access the actual entity
        // Using $this->entity;
        return $this->entity->firstName.' '.$this->entity->lastName;
    }
}

class UserQuery extends \PHPixie\ORM\Wrappers\Type\Database\Query
{
    // Extending queries is useful
    // For adding bulk conditions
    public function popular()
    {
        // Access query with $this->query
        $this->query
            ->where('viewsPerDay', '>', 5000)
            ->orWhere('friendCount' '>=', 100);
    }
}

// You will rarely need to extend repositories
class UserRepository extends \PHPixie\ORM\Wrappers\Type\Database\Repository
{
    // Overriding a save method
    // can be used for validation
    public function save($entity)
    {
        if($entity->getField('name') === null) {
            throw new \Exception("You must provide a user name");
        }

        $this->repository->save($entity);
    }
}

现在我们必须将这些类注册到 ORM 中。

class ORMWrappers extends \PHPixie\ORM\Wrappers\Implementation
{
    // Model names of database entities to wrap
    protected $databaseEntities = array(
        'user'
    );

    // Model names of queries to wrap
    protected $databaseQueries = array(
        'user'
    );

    // Model names of repositories to wrap
    protected $databaseRepositories = array(
        'user'
    );

    // Model names of embedded entities to wrap
    // We cover them later in this manual
    protected $embeddedEntities = array(
        'post'
    );

    // Provide methods to build the wrappers

    public function userEntity($entity)
    {
        return new UserEntity($entity);
    }

    public function userQuery($query)
    {
        return new UserQuery($query);
    }

    public function userRepository($repository)
    {
        return new UserRepository($repository);
    }

    public function postEntity($entity)
    {
        return new PostEntity($entity);
    }
}

在创建 ORM 实例时传递此类的实例

$wrappers = new ORMWrappers();
$orm = new \PHPixie\ORM($database, $slice->arrayData(array(
    // Configuration options
)), $wrappers);

当使用 PHPixie 框架时,您已经注册并存在于您的包中的 ORMWrappers 类。

关系

PHPixie 支持一对一、一对多、多对多关系,以及嵌入式模型的一对一和一对多。每种关系都定义了一组属性,这些属性会被添加到实体和查询中,并可用于访问相关数据。您可以通过配置文件中的 relationships 键来配置它们

return array(
    'models' => array(
        // ...
    ),
    'relationships' => array(
        array(
            'type'  => 'manyToMany',
            'left'  => 'article',
            'right' => 'tag'
        )
    )
);

可以定义同一数据库中表之间的关系,不同数据库之间的关系,甚至是关系数据库和 MongoDB 之间的关系

查询关系

在查看实际的关系类型之前,让我们看看如何定义关系上的条件

// Lets say we have a one to many relationship
// between categories and articles

// Finding all categories with
// at lest one article
//
// Note that you use the property name,
// and not the model name here.
// You can modify property names
// in your config file, more on them later
$categoryQuery->relatedTo('articles');

// or all articles with a category
$articleQuery->relatedTo('category');

// Use logic operators
// like with where()
$articleQuery->orNotRelatedTo('category');

// Find categories related to
// particular articles
$categoryQuery->relatedTo('articles', $articles);

// $articles can be an id of an article,
// an article entity or an article query,
// or an array of them, e.g.
$categoryQuery->relatedTo('articles', $articleQuery);
$categoryQuery->relatedTo('articles', $someArticle);
$categoryQuery->relatedTo('articles', 4); //id

// Will find all categories related
// to any of the defined articles
$categoryQuery->relatedTo('articles', array(
    4, 3, $articleQuery, $articleEntity
));


// Relationship conditions

// Find categories related to articles
// that have a title 'Welcome'
$categoryQuery->relatedTo('articles', function($query) {
    $query
        ->where('title', 'Welcome');
});

// Or a shorthand
// You'll be using this a lot
$categoryQuery->where('articles.title', 'Welcome');

// You can use the '.'
// to go deeper in the relationships

// Find categories that have at least one article
// written by the author 'Dracony'
$categoryQuery->where('articles.author.name', 'Dracony');

// Or combine the verbose approach
// with the shorthand one
$categoryQuery->relatedTo('articles.author', function($query) {
    $query
        ->where('name', 'Dracony');
});

一对一

这是最常见的关系。一个类别可以拥有多个文章,一个主题可以有多个回复等。

// Configuration
return array(
    'models' => array(
        // ...
    ),
    'relationships' => array(
        //...

        array(
            // mandatory options
            'type'  => 'oneToMany',
            'owner' => 'category',
            'items' => 'article',

            // The following keys are optional
            // and will fallback to the defaults
            // based on model names

            'ownerOptions' => array(
                // the name of the property added to the owner
                // e.g. $category->articles();
                //
                // defaults to the plural of the item model
                'itemsProperty' => 'articles'
            ),

            'itemsOptions' => array(

                // the name of the property added to items
                // e.g. $article->category()
                //
                // defaults to the owner model
                'ownerProperty' => 'category',

                // the field that is used to link items
                // defaults to '<property>Id'
                'ownerKey' => 'categoryId',

                // The default behavior is to set
                // the field defined in ownerKey to null
                // when the owner gets deleted
                //
                // changed it to 'delete' to delete
                // the articles when their category is deleted
                'onOwnerDelete' => 'update'
            )
        )
    )
);

现在我们已经定义了关系属性,我们可以开始使用它们了

// Using with entities

// Getting articles category
// The property names used are
// the ones defined in the config
$category = $article->category();

// Get category articles
$articles = $category->articles();

// Add article to category
$category->articles->add($article);

// Remove article from category
$category->articles->remove($article);

// Remove categories from all articles
$category->articles->removeAll();

// Or you can do the same
// from the article side
$article->category->set($category);
$article->category->remove();

注意使用属性而不是像addArticleremoveAllArticles这样的传统方法可以使您的实体更整洁,并且当定义多个关系时,不会导致单个类中添加大量方法

// You can use queries, ids and arrays
// anywhere you can use an entity.
// This allows performing bulk operations faster

// Assign first 5 articles
// to a category
$articleQuery
    ->limit(5)
    ->offset(0);
$category->articles->add($articleQuery);

查询也具有属性,就像实体一样。这使得您可以使用更少的数据库调用执行更多操作。

// Assign articles to a category
$articlesQuery->category->set($category);

// Assign articles to a category with a single
// database call, without the need to select
// rows from database
$categoryQuery->where('title', 'PHP');
$articlesQuery->category->set($category);

// Unset categories for all articles
$orm->query('aricle')
    ->category->remove();

// Unset only some ctaegories
$categoryQuery->where('title', 'PHP');
$orm->query('aricle')
    ->category->remove($category);

使用查询而不是遍历实体可以为您的性能提供巨大的提升

// You can also use query properties
// for query building
$categoryQuery = $articleQuery
    ->where('title', 'Welcome')
    ->categories();

// is the same as
$categoryQuery = $orm->query('category')
    ->relatedTo('articles.title', 'Welcome');

// or
$articleQuery->where('title', 'Welcome');
$categoryQuery = $orm->query('category')
    ->relatedTo('articles', $articleQuery);

在此阶段可能看起来有很多选项,但您只需坚持您最喜欢的任何语法即可。生成的查询保持完全相同。

一对多

一对一关系与一对多关系非常相似。主要区别在于,一旦将新项目附加到拥有者,之前的项目就会被解除。一个很好的例子是拍卖品与最高出价人的关系。一旦我们设置了新的最高出价人,旧的一个就会被取消。另一个例子是任务和工人。每个任务可以由单个工人执行。

// Configuration
return array(
    'models' => array(
        // ...
    ),
    'relationships' => array(
        //...

        array(
            // mandatory options
            'type'  => 'oneToOne',
            'owner' => 'worker',
            'item' => 'task',

            // The following keys are optional
            // and will fallback to the defaults
            // based on model names

            'ownerOptions' => array(
                // the name of the property added to the owner
                // e.g. $worker->task();
                //
                // Unlike manyToMany this uses
                // a singular case by default
                'itemProperty' => 'task'
            ),

            // note it's 'itemOptions' here
            // but 'itemsOptions' for One To Many
            'itemOptions' => array(

                // the name of the property added to items
                // e.g. $task->worker()
                'ownerProperty' => 'worker',

                // the field that is used to link items
                // defaults to '<property>Id'
                'ownerKey' => 'workerId',

                // The default behavior is to set
                // the field defined in ownerKey to null
                // when the owner gets deleted
                //
                // changed it to 'delete' to delete
                // the task when its worker is deleted
                'onOwnerDelete' => 'update'
            )
        )
    )
);
// Using with entities

// Getting task worker
$worker = $task->worker();

// Getting worker tasks
$worker = $task->worker();

// assign worker to a task
$task->worker->set($worker);
$worker->task->set($task);

// Unset task
$task->worker->remove();
$work->task->remove();

一对一关系中的接口在双方都是相同的,并且与一对多关系中的拥有者属性相同

多对多

最常见的多对多关系是文章和标签之间的关系。这些关系需要特殊的枢纽表(或MongoDB集合)来存储项目之间的链接。

// Configuration
return array(
    'models' => array(
        // ...
    ),
    'relationships' => array(
        //...

        array(
            // mandatory options
            'type'  => 'manyToMany',
            'left'  => 'article',
            'right' => 'tag',

            // The following keys are optional
            // and will fallback to the defaults
            // based on model names

            'leftOptions' => array(
                'property' => 'tags'
            ),

            'rightOptions' => array(
                'property' => 'articles'
            ),

            // depends on property names
            // defaults to <rightProperty><leftProperty>
            'pivot' => 'articlesTags',


            'pivotOptions' => array(

                // defaults to the connection
                // of the left model
                'connection' => 'default',

                // columns in pivot table
                // default to '<property>Id'
                'leftKey'  => 'articleId',
                'rightKey' => 'tagId',
            )
        )
    )
);

使用多对多关系类似于使用一对多的拥有者方。

// Add
$article->tags->add($tag);

// Remove a particular tag
$article->tags->remove($tag);

// Remove all tags from article
$article->tags->removeAll();

// Remove all tags from multiple articles
$orm->query('article')
    ->where('status', 'published')
    ->tags->removeAll();

// Construct a tag query from article query
$tagQuery = $orm->query('article')
    ->where('status', 'published')
    ->tags();

// Everything else can be used
// in the same way as categories
// in the one-to-many examples

在这里使用查询进行批量操作,可以使得通过单个数据库调用分配和删除关系成为可能

// Link multiple articles
// to multiple tags in one go
$articleQuery->tags->add($tagQuery);

为优化这些查询操作投入了大量工作,目前没有其他PHP ORM支持在查询之间编辑关系。而不是需要m*n次查询来编辑多对多关系,查询方法可以一次完成。

MongoDB 中的嵌入式模型

MongoDB支持嵌套文档,这允许使用嵌入式模型。例如,文章的作者

{
    "title" : "Welcome",
    //...

    "author" : {
        "name" : "Dracony",
        //...
    }
}

嵌入式模型只包含实体,没有存储库或查询。子文档和子数组不是自动注册为嵌入式关系的,您需要在配置中完成

return array(
    'models' => array(

        // Some database models
        'article' => array(),
        'topic'   => array(),

        // Configuring an embedded model
        'author' => array(
            'type' => 'embedded'
        ),
    ),

    'relationships' => array(
        array(
            'type'  => 'embedsOne',
            'owner' => 'article',
            'item'  => 'author'
        ),

        array(
            'type'  => 'embedsOne',
            'owner' => 'topic',
            'item'  => 'author'
        ),
    )
);

嵌入式和数据库模型之间也有一些使用上的差异

// Embedded entities cannot be saved on their own.
// Instead you just save the database entity
$article->author()->name = 'Dracony';
$article->save();

// Getting the parent model
// Conditions are specified as usual
$articleQuery
    ->where('author.name', 'Dracony');

// To specify a subdocument condition
// use a '>' separator.
//
// This will require the article author
// to have a stats.totalPost field 'Dracony'
$articleQuery
    ->where('author>stats.totalPost', 'Dracony');

嵌入一个

这是一个与上述文章作者示例类似的子文档关系。

// Configuration
return array(
    'models' => array(
        // ...
    ),
    'relationships' => array(
        //...

        array(
            // mandatory options
            'type'  => 'embedsOne',
            'owner' => 'article',
            'item'  => 'author',

            // The following keys are optional
            // and will fallback to the defaults
            // based on model names

            'ownerOptions' => array(
                // the name of the property added to the owner
                // e.g. $article->author();
                //
                // Defaults to item model name
                'property' => 'task',
            ),

            'itemOptions' => array(

                // Dot separated path
                // to the document within owner
                //
                // Defaults to owner property name
                'path' => 'author'

                // You can use nested paths
                // e.g. 'authors.editor'
            )
        )
    )
);

注意我们没有为从作者访问文章定义属性。这是因为嵌入式实体始终使用$entity->owner()来访问所有者。

// get author from article
$author = $article->author();

// get author owner
$article = $author->owner();

// remove author
$article->author->remove();

// Set an author
$article->author->set($author);

// Check if an author is set
$article->author->exists();

// Create and set new author
$author = $article->author->create();

// Create with data
$author = $article->author->create($data);

嵌入多个

定义与子文档数组的关系。例如,论坛主题回复

{
    "title"   : "Welcome",
    "replies" : [
        {
            "message" : "Hello",
            "author"  : "Dracony"
        },
        {
            "message" : "World",
            "author"  : "Dracony"
        }
    ]
}
// Configuration
return array(
    'models' => array(
        // ...
    ),
    'relationships' => array(
        //...

        array(
            // mandatory options
            'type'   => 'embedsMany',
            'owner'  => 'topic',
            'items'  => 'reply',

            // The following keys are optional
            // and will fallback to the defaults
            // based on model names

            'ownerOptions' => array(
                // the name of the property added to the owner
                // e.g. $topic->replies();
                //
                // Defaults to the plural
                // of the item model name
                'property' => 'replies',
            ),

            'itemOptions' => array(

                // Dot separated path
                // to the document within owner
                //
                // Defaults to owner property name
                'path' => 'replies'

                // You can use nested paths
                // e.g. 'content.replies'
            )
        )
    )
);
// Get replies iterator
$replies = $topic->replies();

// Get reply count
$topic->replies->count();

// Get reply by offset
$reply = $topic->replies->get(2);

// Create and add a reply
$reply = $topic->replies->create();

// Create reply from data
$reply = $topic->replies->create($data);

//Create reply at offset
$reply = $topic->replies->create($data, 2);

// Add reply
$topic->replies->add($reply);

// Add reply by offset
$topic->replies->add($reply, 2);

// Remove reply
$topic->replies->remove($reply);

// Remove multiple replies
$topic->replies->remove($replies);

// Remove reply by offset
$topic->replies->offsetUnset(2);

// Remove all replies
$topic->replies->removeAll();

// Check if a reply exists
$exists = $topic->replies->offsetExists(2);

// array access
$reply = $topic->replies[1];
$topic->replies[2] = $reply;

$exists = isset($topic->replies[2]);
unset($topic->replies[2]);

嵌套集合

嵌套集是存储SQL数据库中树的有效方法。一个很好的例子是存储类别和评论树。PHPixie是唯一一个通过命名空间子树来进一步优化它的PHP框架,这使得树插入和更新更快。其背后的代码比通常的嵌套集实现更复杂,但对于用户来说非常简单。

// Configuration
return array(
    'models' => array(
        // ...
    ),
    'relationships' => array(
        //...

        array(
            // mandatory options
            'type'  => 'nestedSet',
            'model'  => 'category',

            // Nested Set requires additional
            // fields to be present in your table.
            // All of them are INTEGER
            // These are their default names
            'leftKey'   => 'left',
            'rightKey'   => 'right',
            'depthKey'  => 'depth',
            'rootIdKey' => 'rootId',

            // You can also customize the
            // relationship property names
            'parentProperty' => 'parent',
            'childrenProperty' => 'children',

            // Defaults to "all<plural of parentProperty>"
            'allParentsProperty' => 'allParents'

            // Defaults to "all<childrenProperty>"
            'allChildrenProperty' => 'allChildren'
        )
    )
);

这种关系定义了四个关系属性而不是通常的两个。`children`属性指向直接子代,而`allChildren`表示节点的所有子代。同样,`parent`是直接父代,而`allParents`与节点的所有父代相关。

基本用法简单,类似于一对一关系。

// Move child to parent
$category->children->add($subcategory);

// or
$subcategory->parent->set($category);

// Remove child from parent
$subcategory->parent->remove();

// Remove all children from node
$category->children->removeAll();

// Find all root nodes and preload
// their children recursively.
$categories = $orm->query('category')
    ->notRelatedTo('parent') // root nodes have no parents
    ->find(array('children'));

一些更高级的用法

// Get a query representing
// all children recursively
$allChildrenQuery = $category->children->allQuery();
$allChidlren = $allChildrenQuery->find();

// Same with getting all parents parents
$allParents = $category->parent->allQuery()->find();

// Find a category with name 'Trees' that is
// a descendant of 'Plants'
$query
    ->where('name', 'Trees')
    ->relatedTo('allParents', function($query) {
        $query->where('name', 'Plants');
    })
    ->find();
    
// or like this
$query
    ->where('name', 'Plants')
    ->allChildren()
        ->where('name', 'Trees')
        ->find();

删除节点时必须特别注意。PHPixie只允许您删除没有子代或子代也被删除的项目。例如

$plants->children->add($trees);
$plants->children->add($flowers);

// An exception will be thrown
$plants->delete();

// move children away from the node
$plants->children->removeAll();
//now it's safe to delete
$plants->delete();

// or delete all three,
$query->in(array($plants, $trees, $flowers))->delete();

重要的是要记住,`parent`和`children`只指直接父代和子代,而`allParents`和`allChildren`递归地指所有相关节点。