devster / alice
Requires
- php: >=5.3
- fzaninotto/faker: ~1.0
- symfony/yaml: ~2.0
Requires (Dev)
- doctrine/common: ~2.3
- phpunit/phpunit: 3.7.*
- symfony/property-access: ~2.2
README
Alice允许您在开发或测试项目时创建大量的测试数据。它提供了几个基本工具,使您能够以易于阅读和编辑的方式生成具有约束的复杂数据,以便团队中的每个人都可以根据需要调整测试数据。
安装
您可以通过Composer安装它,作为nelmio/alice
composer require nelmio/alice
在Symfony2中使用时,您可能想使用hautelook/alice-bundle或h4cc/alice-fixtures-bundle包。
目录
使用方法
基本使用
使用此功能最简单的方法是调用静态方法Nelmio\Alice\Fixtures::load
。它将为您启动一切,并返回一个包含持久对象的容器。
示例
// load a yaml file into a Doctrine\Common\Persistence\ObjectManager object $objects = \Nelmio\Alice\Fixtures::load(__DIR__.'/fixtures.yml', $objectManager); // load a php file into a Doctrine\Common\Persistence\ObjectManager object $objects = \Nelmio\Alice\Fixtures::load(__DIR__.'/fixtures.php', $objectManager);
注意:如果您有多个文件并且这些文件跨越多个引用,您也可以传递一个文件名数组。
选项
Fixtures::load
接受一个第三个参数$options
,它是一个包含以下键的数组
- locale: 默认区域设置
- providers: 一个额外的Faker提供者数组
- seed: 确保Faker生成数据在运行之间一致性的种子,设置为null以禁用(默认为1)
- logger: 一个可调用的或Psr\Log\LoggerInterface对象,将在加载测试数据时接收进度信息
- persist_once: 如果传递多个文件,则只持久化对象一次,默认情况下在每个文件后持久化对象
详细使用
如果您想有更多控制权,可以自己实例化各种对象并轻松使用它们
// load objects from a yaml file $loader = new \Nelmio\Alice\Loader\Yaml(); $objects = $loader->load(__DIR__.'/fixtures.yml'); // optionally persist them into the doctrine object manager // you can also do that yourself or persist them in another way // if you do not use doctrine $persister = new \Nelmio\Alice\ORM\Doctrine($objectManager); $persister->persist($objects);
注意:要加载纯PHP文件,您可以使用
\Nelmio\Alice\Loader\Base
类。这些PHP文件必须返回一个包含与yaml文件相同结构的数组。
参考
创建测试数据
此库最基本的功能是将平面yaml文件转换为对象。您可以在一个文件中定义许多不同类的对象,如下所示
Nelmio\Entity\User: user0: username: bob fullname: Bob birthDate: 1980-10-10 email: bob@example.org favoriteNumber: 42 user1: username: alice fullname: Alice birthDate: 1978-07-12 email: alice@example.org favoriteNumber: 27 Nelmio\Entity\Group: group1: name: Admins
这可以正常工作,但它不是很强大,而且是完全静态的。您仍然需要做大部分工作。让我们看看如何让这更有趣。
测试数据范围
第一步是让Alice为您创建多个对象的副本,以从yaml文件中删除重复项。
您可以通过在测试数据名称中定义一个范围来实现这一点
Nelmio\Entity\User: user{1..10}: username: bob fullname: Bob birthDate: 1980-10-10 email: bob@example.org favoriteNumber: 42
现在它将生成十个用户,名字从user1到user10。相当不错,但我们只有10个相同的名字、用户名和电子邮件的bob,这还不是那么花哨。
您也可以指定一个值列表而不是范围
Nelmio\Entity\User: user{alice, bob}: username: <current()> fullname: <current()> birthDate: 1980-10-10 email: <current()>@example.org favoriteNumber: 42
要更进一步,我们可以随机化数据。
Faker数据
Alice与Faker库集成。使用<foo()>
您可以通过调用Faker数据提供者来生成随机数据。查看Faker提供者列表。
让我们将静态的bob用户转换成一个随机条目
Nelmio\Entity\User: user{1..10}: username: <username()> fullname: <firstName()> <lastName()> birthDate: <date()> email: <email()> favoriteNumber: <numberBetween(1, 200)>
如您在最后一行所见,您也可以像调用函数一样传递参数。
要将Faker数据传递给另一个Faker提供者,您可以在Faker调用中使用$fake()
闭包。例如,使用$fake('firstName', 'de_DE')
或$fake('numberBetween', null, 1, 200)
来调用Faker。传递要调用的提供者,然后是区域设置(或null),然后是传递给提供者的参数。
在简单的PHP固定数据中,$fake
闭包也是可用的。
本地化假数据
Faker可以为地址、电话号码等创建本地化数据。您可以通过在Fixtures::load的$options
数组中传递一个locale
值来设置默认的区域设置。
此外,您还可以通过在faker键中添加区域设置前缀来混合区域设置,例如<fr_FR:phoneNumber()>
或<de_DE:firstName()>
。
默认提供者
Alice包括一个默认的身份提供者<identity()>
,它简单地返回传递给它的任何内容。这使您可以在使用变量替换的同时使用PHP表达式。
为此提供了一些语法糖,<($whatever)>
是<identity($whatever)>
的别名。
调用方法
有时您需要调用一个方法来初始化更多数据,您可以像处理属性一样这样做,但使用方法名并传递一个参数数组。例如,假设用户类有一个需要纬度和经度的setLocation
方法。
Nelmio\Entity\User: user1: username: <username()> setLocation: [40.689269, -74.044737]
指定构造函数参数
当一个构造函数有必需的参数时,您必须按照上述方式定义它,例如,如果用户在构造函数中需要用户名,您可以这样做:
Nelmio\Entity\User: user1: __construct: [<username()>]
如果您想调用静态工厂方法而不是构造函数,可以将哈希作为构造函数指定。
Nelmio\Entity\User: user1: __construct: { create: [<username()>] }
如果您用false
代替构造函数参数,Alice将实例化对象而不执行构造函数。
Nelmio\Entity\User: user1: __construct: false
可选数据
某些字段不需要填写,例如,此示例中的favoriteNumber
可能是不想分享的个人数据,为了在固定数据中反映这一点并确保网站在用户不输入喜欢的数字时也能正常工作,我们可以使用50%? value : empty value
表示法让Alice有时填充它。它有点像三元运算符,并且如果null是可以接受的,则可以省略空值:50%? value
。
让我们使用这些新信息更新用户定义
Nelmio\Entity\User: user{1..10}: username: <username()> fullname: <firstName()> <lastName()> birthDate: <date()> email: <email()> favoriteNumber: 50%? <numberBetween(1, 200)>
现在只有一半的用户会填写数字。
引用
让我们回到组。理想情况下,一个组应该有成员,Alice允许您从一个对象中引用另一个对象。您可以使用@name
表示法来完成此操作,其中name是任何类的固定名称。
让我们给组添加一个固定的所有者
Nelmio\Entity\User: # ... Nelmio\Entity\Group: group1: name: Admins owner: @user1
Alice还允许您使用@name->property
表示法直接引用对象的属性。
Nelmio\Entity\User: # ... Nelmio\Entity\Group: group1: name: Admins owner: @user1->username
为了能够使用此功能,您的实体必须满足一些要求
- 您可以引用公共属性
- 您可以引用通过getter可访问的属性(即:
@name->property
将在property
不是公共的时调用$name->getProperty()
) - 您可以引用实体的ID,但您将不得不将固定数据分成多个文件(这是因为对象在文件处理的末尾持久化)
# fixture_user.yml Nelmio\Entity\User: # ...
# fixture_group.yml Nelmio\Entity\Group: group1: name: Admins owner: @user1->id
如果您想创建十个用户和十个组,并且每个用户拥有一个组,您可以使用<current()>
,它在使用固定数据范围时替换为每个迭代的当前ID。
Nelmio\Entity\User: # ... Nelmio\Entity\Group: group{1..10}: owner: @user<current()>
如果您想使用随机用户而不是固定用户,您可以定义一个带有通配符的引用。
Nelmio\Entity\User: # ... Nelmio\Entity\Group: group1: name: Admins owner: @user*
它将选择任何名称匹配 user*
的对象,其中 *
可以是任何字符串。
有一个限制,您只能引用在文件中定义的对象。如果您想使用数据库中已经存在的对象,也可以提供该对象的ID。但是,为了使其生效,该属性的setter方法必须具有类型提示。
Nelmio\Entity\Group: group1: owner: 1 # this will try to fetch the User (as typehinted in Group::setOwner) with id 1
也可以通过ID创建与随机对象的关联。
Nelmio\Entity\Group: group1: owner: <numberBetween(1, 200)>
注意:要创建一个不是引用的字符串
@foo
,可以将其转义为\@foo
多个引用
如果我们还想添加群组成员,有两种方法。一种是为固定成员集定义一个引用数组
Nelmio\Entity\User: # ... Nelmio\Entity\Group: group1: name: Admins owner: @user1 members: [@user2, @user3]
另一种更有趣的方法是定义一个带有通配符的引用,并告诉Alice您想要多少个对象
Nelmio\Entity\User: # ... Nelmio\Entity\Group: group1: name: Admins owner: @user1 members: 5x @user*
在这种情况下,它将选择5个名称匹配 user*
的固定对象。
您还可以通过结合使用伪造数据来随机化数量。
# ... members: <numberBetween(1, 10)>x @user*
注意:您不需要在数组内部定义多引用,因为它们会自动转换为对象数组。
自我引用
@self
引用被分配给当前固定实例。
传递引用到提供者
您可以像传递 变量 一样传递引用。
Nelmio\Entity\Group: group1: owner: <numberBetween(1, 200)> group2: owner: <numberBetween(@group1->owner, 200)>
处理唯一约束
通常,某些数据库字段上设置了唯一约束,在这种情况下,固定随机失败生成可能会非常讨厌。这在生成大量对象时尤为重要,否则您可能永远不会遇到这个问题。
通过在末尾使用 (unique)
标志声明键为唯一,Alice将确保创建的该类每个元素都具有该属性的唯一值。例如
Nelmio\Entity\User: user{1..10}: username (unique): <username()>
固定继承
基固定,可从中扩展,可以创建来只需要在一系列公共固定定义中定义较少的附加值。
通过使用 (template)
标志声明固定为模板,Alice将实例设置为该文件的模板。模板实例不会持久化。
模板还可以利用继承,通过从其他模板扩展,允许您创建、混合和匹配模板。例如
Nelmio\Entity\User: user_bare (template): username: <username()> user_full (template, extends user_bare): name: <firstName()> lastname: <lastName()> city: <city()>
模板可以通过使用 (extends)
标志后跟要扩展的模板名称来由其他固定扩展。
Nelmio\Entity\User: user (template): username: <username()> age: <numberBetween(1, 20)> user1 (extends user): name: <firstName()> lastname: <lastName()> city: <city()> age: <numberBetween(1, 50)>
继承还允许从多个模板扩展。最后声明的 extends
将始终覆盖先前声明的 extends
模板中的值。然而,扩展属性永远不会覆盖在固定规范中明确设置的值。
在以下示例中,user_young
中的年龄将覆盖 user
中的年龄,而用户名将保持为 user1。
Nelmio\Entity\User: user (template): username: <username()> age: <numberBetween(1, 40)> user_young (template): age: <numberBetween(1, 20)> user1 (extends user, extends user_young): username: user1 name: <firstName()> lastname: <lastName()> city: <city()>
包含文件
您可以使用顶级 include
键包含其他固定文件
include: - relative/path/to/file.yml - relative/path/to/another/file.yml Nelmio\Entity\User: user1 (extends user, extends user_young): name: <firstName()> lastname: <lastName()> city: <city()>
在 relative/path/to/file.yml
Nelmio\Entity\User: user (template): username: <username()> age: <numberBetween(1, 40)>
在 relative/path/to/another/file.yml
Nelmio\Entity\User: user_young (template): age: <numberBetween(1, 20)>
在生成之前将所有文件合并到一个数据集中,并且在出现重复键的情况下,包含文件的内容将优于被包含文件的固定。
变量
对于某些高级用例,您有时需要从另一个属性引用一个属性,例如在确保它是 之后 创建日期的同时生成更新日期。如果您简单地使用两个随机日期,它们可能会颠倒,但Alice允许您使用传统的PHP $variable
表示法引用其他属性。
让我们向我们的群组添加创建/修改日期
Nelmio\Entity\User: # ... Nelmio\Entity\Group: group1: name: Admins owner: @user1 members: <numberBetween(1, 10)>x @user* created: <dateTimeBetween('-200 days', 'now')> updated: <dateTimeBetween($created, 'now')>
如您所见,我们确保更新日期在创建日期和当前时间之间,这样可以确保数据看起来足够真实。
值对象
有时你需要一些不是由ORM持久化的值对象,但只是存储在其他对象上。你可以在类或实例名称上使用(local)
标志来标记它们为不可持久化。它们将作为引用在其他对象中使用,但不会被LoaderInterface::load
调用返回。
例如,这可以避免在使用Doctrine持久化器时由于Geopoint不是实体而引发错误。
Nelmio\Data\Geopoint (local): geo1: __construct: [<latitude()>, <longitude()>] Nelmio\Entity\Location: loc{1..100}: name: <city()> geopoint: @geo1
自定义Faker数据提供者
有时你需要的不仅仅是Faker和Alice提供的原生功能,有两种方法可以解决这个问题
-
在yaml文件中嵌入PHP代码。由加载器包含,因此只要输出有效的yaml,你就可以添加任意PHP代码。换句话说,这就像PHP模板,如果你做得太多逻辑,很快就会变得非常混乱,所以最好将逻辑从模板中提取出来。
-
添加自定义Faker提供者类。这些只是公开方法的类,所有公开方法都作为
<method()>
在Alice fixture文件中可用。例如,如果你需要一个自定义组名生成器,并在一个Symfony2项目中使用标准的Doctrine Fixtures包,你可以这样做<?php namespace Acme\DemoBundle\DataFixtures\ORM; use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\DataFixtures\FixtureInterface; use Nelmio\Alice\Fixtures; class LoadFixtureData implements FixtureInterface { public function load(ObjectManager $om) { // pass $this as an additional faker provider to make the "groupName" // method available as a data provider Fixtures::load(__DIR__.'/fixtures.yml', $om, array('providers' => array($this))); } public function groupName() { $names = array( 'Group A', 'Group B', 'Group C', ); return $names[array_rand($names)]; } }
这样你就可以现在使用
name: <groupName()>
来生成特定的组名。
自定义设置器
如果,你想要指定一个用于设置所有值的自定义函数,你可以指定一个__set
值
Nelmio\Data\Geopoint: geo1: __set: customSetter foo: bar
当对象被填充时,customSetter
函数将被调用,第一个参数是key
,第二个是value
(与PHP的魔术setter类似)。在上面的例子中,当填充实例时将进行以下调用
$geopoint->customSetter('foo', 'bar');
完整示例
最后,使用上述大多数工具,我们可以创建一个文件,其中创建了一群用户和一个组,所有这些都被链接在一起,并且几乎不需要打字
Nelmio\Entity\User: user{1..10}: username: <username()> fullname: <firstName()> <lastName()> birthDate: <date()> email: <email()> favoriteNumber: 50%? <numberBetween(1, 200)> Nelmio\Entity\Group: group1: name: Admins owner: @user1 members: <numberBetween(1, 10)>x @user* created: <dateTimeBetween('-200 days', 'now')> updated: <dateTimeBetween($created, 'now')>
如果你想要一些具有特定数据的特定用户来编写测试,当然你可以在使用随机数据定义的上面或下面定义它们。按需组合它们!
处理器
处理器允许你在对象被持久化之前和/或之后处理它们。处理器必须实现ProcessorInterface
。
以下是一个示例,说明当使用FOSUserBundle时我们可能使用此功能
namespace Acme\DemoBundle\DataFixtures\ORM; use FOS\UserBundle\Model\UserInterface; use FOS\UserBundle\Model\UserManager; use Nelmio\Alice\ProcessorInterface; use Symfony\Component\DependencyInjection\ContainerInterface; class UserProcessor implements ProcessorInterface { /** * @var ContainerInterface */ protected $container; /** * @param ContainerInterface $container */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * {@inheritdoc} */ public function preProcess($object) { } /** * {@inheritdoc} */ public function postProcess($object) { if (!($object instanceof UserInterface)) { return; } /** @var UserManager $manager */ $manager = $this->container->get('fos_user.user_manager'); $manager->updateUser($object); } }
你可以在加载方法中添加处理器列表,例如
$objects = \Nelmio\Alice\Fixtures::load(__DIR__.'/fixtures.yml', $objectManager, $options, $processors);
或者,你可以使用addProcessor()
方法将它们添加到你的加载器中,例如
$loader = new \Nelmio\Alice\Fixtures($objectManager, $options); $loader->addProcessor($processor); $objects = $loader->load(__DIR__.'/fixtures.yml');
许可证
在MIT许可证下发布,请参阅LICENSE。