level-2/maphper

Maphper 是一个轻量级的 ORM,采用 DataMapper 模式,可以动态创建数据库表。

v0.9.3.0 2020-08-16 17:47 UTC

This package is auto-updated.

Last update: 2024-09-17 02:28:51 UTC


README

Maphper - 使用 Data Mapper 模式的 PHP ORM

仍在开发中!

Scrutinizer-CI

功能

  1. 动态创建数据库表

  2. 目前支持数据库表,但将支持 XML 文件、Web 服务甚至 Twitter 流作为数据源

  3. 支持任何两个数据源之间的关系

  4. 复合主键

Maphper 采用简单和极简的方法进行数据映射,旨在为最终用户提供直观且易于使用的 API。

Maphper 的主要哲学是程序员应该处理数据集,然后能够操作这个集合并从中提取其他数据。

基本用法

这设置了一个数据映射器,将其映射到 MySQL 数据库中的 'blog' 表。每个映射器都有一个数据源。这个源可以是任何东西,比如数据库表、XML 文件、充满 XML 文件的文件夹、CSV、Twitter 流、Web 服务或任何东西。

目标是给开发者提供一个一致且简单的 API,该 API 在任何数据源之间共享。

然后可以像数组一样处理 $blogs 对象。

要使用数据库表作为其数据源设置 Maphper 对象,首先创建一个标准的 PDO 实例

$pdo = new PDO('mysql:dbname=maphpertest;host=127.0.0.1', 'username', 'password');

然后创建一个 \Maphper\DataSource\Database 的实例,传入 PDO 实例、表名和主键

$blogSource = new \Maphper\DataSource\Database($pdo, 'blog', 'id');

最后创建一个 \Maphper\Maphper 的实例,传入数据源

$blogs = new \Maphper\Maphper($blogSource);

您可以使用以下方式遍历所有博客

// Equivalent to SELECT * FROM blogs
foreach ($blogs as $blog) {
  echo $blog->title . '<br />';
}

数据库表(或 XML 文件、Web 服务等)中的任何字段都将可用在 $blog 对象中

或者您可以使用 ID 查找特定的博客

//Equivalent to SELECT * FROM blogs WHERE id = 142
echo $blogs[142]->title;

这将找到 id 为 142 的博客并显示标题。

过滤器

Maphper 支持过滤数据

//find blogs that were posted on a specific date
//Equivalent to SELECT * FROM blogs WHERE date = '2015-04-09'
$filteredBlogs = $blogs->filter(['date' => '2014-04-09']);

//this will only retrieve blogs that were matched by the filter
foreach ($filteredBlogs as $blog) {
	echo $blog->title;
}

过滤器可以扩展并串联在一起

//find blogs that were posted with the title "My Blog" by the author with the id of 7
//Equivalent to SELECT * FROM blogs WHERE title = 'My Blog' AND authorId = 7 
$filteredBlogs = $blogs->filter(['title' => 'My Blog'])->filter(['authorId' => 7]);

//this will only retrieve blogs that were matched by both filters
foreach ($filteredBlogs as $blog) {
	echo $blog->title;
}

除了过滤之外,还有 sort() 和 limit() 方法,都可以串联使用

foreach ($blogs->filter(['date' => '2014-04-09']) as $blog) {
  echo $blog->title;
}

要找到最新的 5 篇博客,可以使用

//Equivalent to SELECT * FROM blogs LIMIT 5 ORDER BY date DESC
foreach ($blogs->limit(5)->sort('date desc') as $blog) {
  echo $blog->title;
}

像任何数组一样,您可以使用以下方法计算博客的总数

//Equivalent to SELECT count(*) FROM lblogs
echo 'Total number of blogs is ' . count($blogs);

这将计算表中的博客总数。您还可以计算过滤结果的数量

//Count the number of blogs in category 3
//Equivalent to SELECT count(*) FROM blogs WHERE categoryId =  3
echo count($blogs->filter(['categoryId' => 3]);

保存数据

要将对象保存回数据映射器,只需创建一个 stdClass 实例

$pdo = new PDO('mysql:dbname=maphpertest;host=127.0.0.1', 'username', 'password');
$blogSource = new \Maphper\DataSource\Database($pdo, 'blog', 'id');
$blogs = new \Maphper\Maphper($blogSource);


$blog = new stdClass;

$blog->title = 'My Blog Title';
$blog->content = 'This is my first blog entry';

//Store the blog using the next available ID
$blogs[] = $blog;

echo 'The new blog ID is :' . $blog->id;

或者,您可以通过指定索引将记录写入特定的 ID

$blog = new stdClass;

$blog->title = 'My Blog Title';
$blog->content = 'This is my first blog entry';

//Store the blog with the primary key of 7
$blogs[7] = $blog;

注意:这种行为与直接在 $blog 对象上设置 id 属性的行为相同

$blog = new stdClass;

$blog->id = 7;
$blog->title = 'My Blog Title';
$blog->content = 'This is my first blog entry';

//Store the blog with the primary key of 7
$blogs[] = $blog;

关系

如果有存储关于博客作者信息的 author 表,可以以类似的方式设置

$authorSource = new \Maphper\DataSource\Database($pdo, 'author', 'id');
$authors = new \Maphper\Maphper($authorSource);

可以使用类似的方式进行使用

$author = $authors[123];
echo $author->name;

一旦定义了博客和作者映射器,您可以在它们之间创建一个关系。

这是一个一对一关系(一个博客有一个作者),可以使用博客映射器上的 addRelation 方法实现

//Create a one-to-one relationship between blogs and authors (a blog can only have one author)
$relation = new \Maphper\Relation\One($authors, 'authorId', 'id');
$blogs->addRelation('author', $relation);

使用 \Maphper\Relation\One 实例告诉 Maphper 这是一个一对一的关系

第一个参数 $authors 告诉 Maphper 该关系是对 $authors 映射器(在这种情况下,是作者数据库表,尽管您可以在不同的数据源之间定义关系)

authorId 是博客表中要连接的字段

id 是作者表中要连接的字段

关系构建完成后,可以使用以下方法将其添加到博客映射器:

$blogs->addRelation('author', $relation);

第一个参数(此处为'author')是相关数据可用的属性名称。因此,从 $blogs 映射器检索到的任何对象现在都将有一个包含博客作者的 'author' 属性,可以使用如下方式使用:

foreach ($blogs as $blog) {
  echo $blog->title . '<br />';
  echo $blog->author->name . '<br />';
}

类似地,您可以定义作者和博客之间的反向关系。这是一个一对一关系,因为一个作者可以发表多篇博客。

//Create a one-to-many relationship between blogs and authors (an author can have multiple blog entries)
//Joining from the 'id' field in the authors mapper to the 'authorId' field in the blogs mapper
$relation = new \Maphper\Relation\Many($blogs, 'id', 'authorId');
$authors->addRelation('blogs', $relation);

这是通过使用 $authors 映射器中的 id 字段到 $blogs 映射器中的 authorId 字段来创建 $authors$blogs 映射器之间的 One:Many 关系,并为 $authors 映射器返回的任何对象提供一个可用的 blogs 属性。

//Count all the blogs by the author with id 4
$authors[4]->name . ' has posted ' .  count($authors[4]->blogs)  . ' blogs:<br />';

//Loop through all the blogs created by the author with id 4
foreach ($authors[4]->blogs as $blog) {
    echo $blog->title . '<br />';
}

保存具有关系的值

创建映射器和定义它们之间的关系后,您可以使用关系写入数据。这将在幕后自动设置任何相关字段。

$authors = new \Maphper\Maphper(new \Maphper\DataSource\Database($pdo, 'author'));
$blogs = new \Maphper\Maphper(new \Maphper\DataSource\Database($pdo, 'blog', 'id'));
$blogs->addRelation('author', new \Maphper\Relation\One($authors, 'authorId', 'id'));


$blog = new stdClass;
$blog->title = 'My First Blog';
$blog->date = new \DateTime();
$blog->author = new stdClass;
$blog->author->name = 'Tom Butler';

$blogs[] = $blog;

这将同时将 $blog 对象保存到 blog 表中,将 $author 对象保存到 author 表中,并将博客记录的 authorId 列设置为生成的 id。

您也可以用一对一关系做同样的事情。

$authors = new \Maphper\Maphper(new \Maphper\DataSource\Database($pdo, 'author'));
$blogs = new \Maphper\Maphper(new \Maphper\DataSource\Database($pdo, 'blog', 'id'))

$authors->addRelation('blogs', new \Maphper\Relation\Many($blogs, 'id', 'authorId'));


//Find the author with id 4
$author = $authors[4]; 


$blog = new stdClass;
$blog->title = 'My New Blog';
$blog->date = new \DateTime();

//Add the blog to the author. This will save to the database at the this point, you do not need to explicitly 
//Save the $author object after adding a blog to it.
$author->blogs[] = $blog;

复合主键

Maphper 允许复合主键。例如,如果您有一个产品表,您可以使用制造商 id 和制造商零件号作为主键(两个制造商可能使用相同的零件号)。

为此,使用一个数组定义主键的数据源。

$pdo = new PDO('mysql:dbname=maphpertest;host=127.0.0.1', 'username', 'password');
$productSource = new \Maphper\DataSource\Database($pdo, 'products', ['manufacturerId', 'partNumber']);
$products = new \Maphper\Maphper($productSource);

一旦您定义了使用多个键的源,您就可以像处理二维数组一样处理 $products 变量。

//Get the product with manufacturerId 7 and partNumber AC294
echo $products[7]['AC294']->name;

要使用复合键写入数据,您只需将一个对象写入指定的索引即可。

$pdo = new PDO('mysql:dbname=maphpertest;host=127.0.0.1', 'username', 'password');
$productSource = new \Maphper\DataSource\Database($pdo, 'products', ['manufacturerId', 'partNumber']);
$products = new \Maphper\Maphper($productSource);

$product = new stdClass;
$product->name 'Can of cola';

$products[1]['CANCOLA'] = $product;

日期

Maphper 使用内置的 PHP \DateTime 类来存储和按日期搜索。

$blog = new stdClass;
$blog->title = 'A blog entry';

//You can construct the date object using any of the formats availble in the inbuilt PHP datetime class
$blog->date = new \DateTime('2015-11-14');

这也可以用于筛选器。

//Find all blogs posted on 2015-11-14
$maphper->filter(['date' => new \DateTime('2015-11-14')]);

建议使用 DateTime 类而不是传递日期值作为字符串,因为并非所有映射器都使用相同的内部日期格式。

自动数据库表创建/修改

Maphper 可以指示自动构建数据库表。这是动态进行的,您只需告诉 Maphper 您想使用这种行为。当您构建数据库数据源时,将 editmode 设置为 true。

$pdo = new PDO('mysql:dbname=maphpertest;host=127.0.0.1', 'username', 'password');
$blogs = new \Maphper\DataSource\Database($pdo, 'blogs', 'id', ['editmode' => true]);

n.b. 'editmode => true' 是 \Maphper\DataSoruce\Database::EDIT_STRUCTURE | \Maphper\DataSoruce\Database::EDIT_INDEX | \Maphper\DataSoruce\Database::EDIT_OPTIMISE; 的缩写。

可用的标志可以互换使用位或,例如 \Maphper\DataSoruce\Database::EDIT_STRUCTURE | \Maphper\DataSoruce\Database::EDIT_INDEX 将启用结构和索引修改,但不会允许列优化。

editmode 的三个选项是

\Maphper\DataSoruce\Database::EDIT_STRUCTURE - 当此选项设置时,Maphper 自动创建不存在的表,在写入尚不存在属性的列时创建列,并在越界列上更改数据类型。

$blog = new stdClass;
$blog->title = 'A blog';
$blog->date = new \DateTime();
$blogs[] = $blog;

对于数据库映射器,这将发出一个 CREATE TABLE 语句,创建一个名为 blogs 的表,具有列 id INT auto_incrementtitle VARCHARdate DATETIME

类型处理

Maphper 将在创建表时使用最严格的类型。例如

$blog = new stdClass;
$blog->title = 1;
$blogs[] = $blog;

这将创建一个作为整数的 title 列,因为只有整数被存储在其中。然而,如果在创建后向表中添加了不同类型的记录

$blog = new stdClass;
$blog->title = 'Another blog';
$blogs[] = $blog;

这将发出一个 ALTER TABLE 查询,将 title 列更改为 varchar。同样,如果添加了非常长的字符串作为标题,该列将更改为 LONGBLOG。所有这些都是在幕后动态完成的,作为开发者,您根本不需要担心表结构。

索引

\Maphper\DataSoruce\Database::EDIT_INDEX 当设置此选项时,Maphper将自动为在WHERE、ORDER和GROUP语句中使用的列添加索引。如果执行多列WHERE查询,还会添加多列索引。

数据库优化

\Maphper\DataSoruce\Database::EDIT_OPTIMISE 当设置此选项时,Maphper会自动定期优化数据库表。例如,一个设置为VARCHAR(255)的列,其中最长条目为7个字符,将被更改为VARCHAR(7);或者一个包含3条记录(值分别为1、2、3)的VARCHAR(255)列将被转换为INT(11)。这还会自动删除所有记录中都包含NULL值的任何列。

目前优化在创建DataSource的500次后发生一次。在未来的版本中,此值将是可配置的。

映射对象的具体类

您可以使用自己的类代替stdClass来管理Maphper的任何对象。当对象被创建时,其属性将自动设置。

例如,如果您有一个产品类

class Product {
	private $name;
	private $price;
	
	const TAX_RATE = 0.2;
	
	public function getTax() {
		return $this->price * self::TAX_RATE;
	}
	
	public function getTotalPrice() {
		return $this->price + $this->getTax();
	}
	
	public function setName($name) {
		$this->name = $name;
	}
}

您可以通过在创建Maphper实例时,在$options数组中使用resultClass选项来指示Maphper使用此类

$dataSource = new \Maphper\DataSource\Database($pdo, 'product', 'id');
$products = new \Maphper\Maphper($dataSource, ['resultClass' => 'Product']);

$product = $products[123];


echo get_class($product); //"Product" instance

//And as expected, the methods from Product are available:
$tax = $product->getTax();
$total = $product->getTotalPrice();

私有属性在保存和加载时与任何正常属性相同。

同样,您也可以创建Product类的实例并将其保存到映射器中。

$product = new Product;
$product->setName('A Product');
//Write the product to the mapper. Even though $product->name is private it will still be stored
$products[] = $product;

新对象的工厂创建

有时您的结果类可能存在依赖关系。在这种情况下,您可以指定一个方法而不是类名作为工厂。考虑以下示例

class TaxCalculator {
	const TAX_RATE = 0.2;
	
	public function getTax($price) {
		return $price * self::TAX_RATE;
	}
}

class Product {
	private $name;
	private $price;
	private $taxCalculator;
	
	
	public function __construct(TaxCalculator $taxCalculator) {
		$this->taxCalculator = $taxCalculator;
	}	
	
	public function getTax() {
		return $this->taxCalculator->getTax($this->price);
	}
	
	public function getTotalPrice() {
		return $this->price + $this->getTax();
	}
}

在这种情况下使用

$dataSource = new \Maphper\Maphper($database, ['resultClass' => 'Product']);

将报错,因为当构造Product实例时,Maphper不足以猜测需要将TaxCalculator实例作为构造函数参数。相反,您可以通过传递一个返回完全构造对象的闭包来解决问题

$taxCalculator = new TaxCalculator;

$dataSource = new \Maphper\Maphper($database, ['resultClass' => function() use ($taxCalculator) {
	return new Product($taxCalculator);
}]);

或者,如果您想要对依赖关系有更多控制,可以使用依赖注入容器,例如Dice

$dice = new \Dice\Dice;

$dataSource = new \Maphper\Maphper($database, ['resultClass' => function() use ($dice) {
	return $dice->create('Product');
}]);

多对多关系

考虑movieactor表,一个演员可以出现在多部电影中,而一部电影也有多个演员。仅使用两个表和主键/外键关系无法建模这种关系。

在关系型数据库(和Maphper)中,这需要存储actorIdmovieId的中间表。

要使用Maphper建模此关系,首先设置标准的actormovie映射器

$actors = new \Maphper\Maphper(new \Maphper\Datasource\Database($pdo, 'actor', 'id'));
$movies = new \Maphper\Maphper(new \Maphper\Datasource\Database($pdo, 'movie', 'id'));

然后添加一个中间表的表。请注意,这需要两个主键,一个用于actorId,一个用于movieId

$cast = new \Maphper\Maphper(new \Maphper\Datasource\Database($pdo, 'cast', ['movieId', 'actorId']));

请注意,Maphper可以在开启editmode的情况下为您创建此表

现在,您可以使用\Maphper\Relation\ManyMany类来设置多对多关系,作为关系的第一个实例

$actors->addRelation('movies', new \Maphper\Relation\ManyMany($cast, $movies, 'id', 'movieId'));

这将在actor对象上创建一个名为movies的关系。

ManyMany类的第一个构造函数参数是中间表。

第二个构造函数参数是要映射的表。

第三个是movies表的主键。

第四个是中间表中的键。

这将连接$actors映射器到$cast映射器,然后是$cast映射器到$movies映射器。

完成此操作后,您还可以设置反向关系,以相同的方式将电影映射到出演它们的演员

$movies->addRelation('actors', new \Maphper\Relation\ManyMany($cast, $actors, 'id', 'actorId'));

一旦这两个关系都设置好了,您就可以添加一个带有电影的演员

$actor = new \stdclass;
$actor->id = 123;
$actor->name = 'Samuel L. Jackson';

//save the actor
$actors[] = $actor;


//now add some movies to the actor
$movie1 = new \stdclass;
$movie1->title = 'Pulp Fiction';

$actor->movies[] = $movie1;


//now add some movies to the actor
$movie2 = new \stdclass;
$movie2->title = 'Snakes on a Plane';

$actor->movies[] = $movie2;

完成此操作后,您可以使用以下方法获取一个演员出演的所有电影

$actor = $actors[123];

echo $actor->name . ' was in the movies:' . "\n";

foreach ($actor->movies as $movie) {
	echo $movie->title . "\n";
}

这将打印

Samuel L. Jackson was in the movies:
Pulp Fiction
Snakes on a Plane

当然,使用正常的一对多关系也可以做到这一点。多对多关系只有在有更多演员时才有用

$actor = new \stdclass;
$actor->id = 124;
$actor->name = 'John Travolta';
$actors[] = $actor;

//Find the movie 'Pulp Fiction and add it to John Travolta
$movie = $movies->filter(['title' =>'Pulp Fiction'])->item(0);
$actor->movies[] = $movie;

现在,您可以使用以下方法找到所有出演《低俗小说》的演员

$movie = $movies->filter(['title' =>'Pulp Fiction'])->item(0);

echo 'The actors in ' . $movie->title . ' are :' . "\n";
foreach ($movie->actors as $actor) {
	echo $actor->name . "\n";
}

这将打印

The actors in Pulp Fiction are:
Samuel L. Jackson
John Travolta

在中间表中存储数据

有时在中间表中存储额外信息很有用。在上面的例子中,了解演员在特定电影中所扮演的角色名称将很有帮助。为此,在设置关系时使用了两个额外字段。

而不是

$actors->addRelation('movies', new \Maphper\Relation\ManyMany($cast, $movies, 'id', 'movieId'));
$movies->addRelation('actors', new \Maphper\Relation\ManyMany($cast, $actors, 'id', 'actorId'));

您可以使用

$actors->addRelation('roles', new \Maphper\Relation\ManyMany($cast, $movies, 'id', 'movieId', 'movie');
$movies->addRelation('cast', new \Maphper\Relation\ManyMany($cast, $actors, 'id', 'actorId', 'actor');

为《\Maphper\Relation\ManyMany》构造函数的第5个参数添加了。

此参数是用于中间表对象的字段名称。当提供此参数时,中间映射器不会自动遍历,而是像父表(例如演员)和中间表(例如演员表)之间的正常一对多关系一样访问:现在可以为演员分配一些角色

在这个例子中,演员有roles,电影有cast。现在这个设置完成后,可以为演员分配一些角色

$actor = $actors[123];


//Find the movie 
$movie = $movies->filter(['title' =>'Pulp Fiction'])->item(0);

//Create a role
$role = new \stdClass;
//Set the character name for the role
$role->characterName = 'Jules Winnfield';
//Assign the movie to the role
$role->movie = $movie;
//Assign the role to the actor
$actor->roles[] = $role;


//Find the movie 
$movie = $movies->filter(['title' =>'Snakes on a Plane'])->item(0);
//Create a role
$role = new \stdClass;
//Set the character name for the role
$role->characterName = 'Neville Flynn';
//Assign the movie to the role
$role->movie = $movie;
//Assign the role to the actor
$actor->roles[] = $role;

这将为Samuel L. Jackson分配了Pulp Fiction和Snakes on a Plane电影的角色。现在可以显示电影了

$actor = $actors[123];

echo $actor->name . ' has the roles:' . "\n"
foreach ($actors[123]->roles as $role) {
    echo $role->characterName . ' in the movie . ' . $role->movie->title . "\n";
}

这将打印出

Samuel L. Jackson has the roles:
Jules Winnfield in Pulp Fiction
Neville Flynn in Snakes on a plane