Merge pull request #2894 from wilr/3.1-manymanyyaml

Add support for many_many_extraField in YAML
This commit is contained in:
Simon Welsh 2014-03-04 11:35:58 +13:00
commit ccb791995e
4 changed files with 247 additions and 65 deletions

View File

@ -128,8 +128,28 @@ class FixtureBlueprint {
// Populate all relations // Populate all relations
if($data) foreach($data as $fieldName => $fieldVal) { if($data) foreach($data as $fieldName => $fieldVal) {
if($obj->many_many($fieldName) || $obj->has_many($fieldName)) { if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
$obj->write();
$parsedItems = array(); $parsedItems = array();
if(is_array($fieldVal)) {
// handle lists of many_many relations. Each item can
// specify the many_many_extraFields against each
// related item.
foreach($fieldVal as $relVal) {
$item = key($relVal);
$id = $this->parseValue($item, $fixtures);
$parsedItems[] = $id;
array_shift($relVal);
$obj->getManyManyComponents($fieldName)->add(
$id, $relVal
);
}
} else {
$items = preg_split('/ *, */',trim($fieldVal)); $items = preg_split('/ *, */',trim($fieldVal));
foreach($items as $item) { foreach($items as $item) {
// Check for correct format: =><relationname>.<identifier>. // Check for correct format: =><relationname>.<identifier>.
// Ignore if the item has already been replaced with a numeric DB identifier // Ignore if the item has already been replaced with a numeric DB identifier
@ -144,12 +164,13 @@ class FixtureBlueprint {
$parsedItems[] = $this->parseValue($item, $fixtures); $parsedItems[] = $this->parseValue($item, $fixtures);
} }
$obj->write();
if($obj->has_many($fieldName)) { if($obj->has_many($fieldName)) {
$obj->getComponents($fieldName)->setByIDList($parsedItems); $obj->getComponents($fieldName)->setByIDList($parsedItems);
} elseif($obj->many_many($fieldName)) { } elseif($obj->many_many($fieldName)) {
$obj->getManyManyComponents($fieldName)->setByIDList($parsedItems); $obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
} }
}
} elseif($obj->has_one($fieldName)) { } elseif($obj->has_one($fieldName)) {
// Sets has_one with relation name // Sets has_one with relation name
$obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures); $obj->{$fieldName . 'ID'} = $this->parseValue($fieldVal, $fixtures);

View File

@ -2,38 +2,42 @@
## Overview ## Overview
You will often find the need to test your functionality with some consistent data. You will often find the need to test your functionality with some consistent
If we are testing our code with the same data each time, data. If we are testing our code with the same data each time, we can trust our
we can trust our tests to yeild reliable results. tests to yield reliable results.
In Silverstripe we define this data via 'fixtures' (so called because of their fixed nature).
The `[api:SapphireTest]` class takes care of populating a test database with data from these fixtures - In Silverstripe we define this data via 'fixtures' (so called because of their
all we have to do is define them, and we have a few ways in which we can do this. fixed nature). The `[api:SapphireTest]` class takes care of populating a test
database with data from these fixtures - all we have to do is define them, and
we have a few ways in which we can do this.
## YAML Fixtures ## YAML Fixtures
YAML is a markup language which is deliberately simple and easy to read, YAML is a markup language which is deliberately simple and easy to read, so it
so it is ideal for fixture generation. is ideal for fixture generation.
Say we have the following two DataObjects: Say we have the following two DataObjects:
:::php :::php
class Player extends DataObject { class Player extends DataObject {
static $db = array (
private static $db = array (
'Name' => 'Varchar(255)' 'Name' => 'Varchar(255)'
); );
static $has_one = array( private static $has_one = array(
'Team' => 'Team' 'Team' => 'Team'
); );
} }
class Team extends DataObject { class Team extends DataObject {
static $db = array (
private static $db = array (
'Name' => 'Varchar(255)', 'Name' => 'Varchar(255)',
'Origin' => 'Varchar(255)' 'Origin' => 'Varchar(255)'
); );
static $has_many = array( private static $has_many = array(
'Players' => 'Player' 'Players' => 'Player'
); );
} }
@ -59,31 +63,42 @@ We can represent multiple instances of them in `YAML` as follows:
Name: The Crusaders Name: The Crusaders
Origin: Bay of Plenty Origin: Bay of Plenty
Our `YAML` is broken up into three levels, signified by the indentation of each line. Our `YAML` is broken up into three levels, signified by the indentation of each
In the first level of indentation, `Player` and `Team`, line. In the first level of indentation, `Player` and `Team`, represent the
represent the class names of the objects we want to be created for the test. class names of the objects we want to be created for the test.
The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers. The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are
These are what you pass as the second argument of `SapphireTest::objFromFixture()`. identifiers. These are what you pass as the second argument of
Each identifier you specify represents a new object. `SapphireTest::objFromFixture()`. Each identifier you specify represents a new
object.
The third and final level represents each individual object's fields. The third and final level represents each individual object's fields.
A field can either be provided with raw data (such as the Names for our Players),
or we can define a relationship, as seen by the fields prefixed with `=>`.
Each one of our Players has a relationship to a Team, A field can either be provided with raw data (such as the names for our
this is shown with the `Team` field for each `Player` being set to `=>Team.` followed by a team name. Players), or we can define a relationship, as seen by the fields prefixed with
Take the player John for example, his team is the Hurricanes which is represented by `=>Team.hurricanes`. `=>`.
This is tells the system that we want to set up a relationship for the `Player` object `john` with the `Team` object `hurricanes`.
Each one of our Players has a relationship to a Team, this is shown with the
`Team` field for each `Player` being set to `=>Team.` followed by a team name.
Take the player John for example, his team is the Hurricanes which is
represented by `=>Team.hurricanes`.
This is tells the system that we want to set up a relationship for the `Player`
object `john` with the `Team` object `hurricanes`.
It will populate the `Player` object's `TeamID` with the ID of `hurricanes`, It will populate the `Player` object's `TeamID` with the ID of `hurricanes`,
just like how a relationship is always set up. just like how a relationship is always set up.
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
Note that we use the name of the relationship (Team), and not the name of the database field (TeamID). Note that we use the name of the relationship (Team), and not the name of the
database field (TeamID).
</div> </div>
This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship. This style of relationship declaration can be used for both a `has-one` and a
For `many-many` relationships, we specify a comma separated list of values. `many-many` relationship. For `many-many` relationships, we specify a comma
separated list of values.
For example we could just as easily write the above as: For example we could just as easily write the above as:
:::yml :::yml
@ -104,27 +119,97 @@ For example we could just as easily write the above as:
Origin: Bay of Plenty Origin: Bay of Plenty
Players: =>Player.joe,=>Player.jack Players: =>Player.joe,=>Player.jack
A crucial thing to note is that **the YAML file specifies DataObjects, not database records**. A crucial thing to note is that **the YAML file specifies DataObjects, not
The database is populated by instantiating DataObject objects and setting the fields declared in the YML, database records**.
then calling write() on those objects.
This means that any `onBeforeWrite()` or default value logic will be executed as part of the test. The database is populated by instantiating DataObject objects and setting the
The reasoning behind this is to allow us to test the `onBeforeWrite` functionality of our objects. fields declared in the YML, then calling write() on those objects. This means
You can see this kind of testing in action in the `testURLGeneration()` test from the example in that any `onBeforeWrite()` or default value logic will be executed as part of
[Creating a SilverStripe Test](creating-a-silverstripe-test). the test. The reasoning behind this is to allow us to test the `onBeforeWrite`
functionality of our objects.
You can see this kind of testing in action in the `testURLGeneration()` test
from the example in [Creating a SilverStripe Test](creating-a-silverstripe-test).
### Defining many_many_extraFields
`many_many` relations can have additional database fields attached to the
relationship. For example we may want to declare the role each player has in the
team.
:::php
class Player extends DataObject {
private static $db = array (
'Name' => 'Varchar(255)'
);
private static $belongs_many_many = array(
'Teams' => 'Team'
);
}
class Team extends DataObject {
private static $db = array (
'Name' => 'Varchar(255)'
);
private static $many_many = array(
'Players' => 'Player'
);
private static $many_many_extraFields = array(
"Players" => array(
"Role" => "Varchar"
);
);
}
To provide the value for the many_many_extraField use the YAML list syntax.
:::yml
Player:
john:
Name: John
joe:
Name: Joe
jack:
Name: Jack
Team:
hurricanes:
Name: The Hurricanes
Players:
- =>Player.john:
Role: Captain
crusaders:
Name: The Crusaders
Players:
- =>Player.joe:
Role: Captain
- =>Player.jack:
Role: Winger
## Test Class Definition ## Test Class Definition
### Manual Object Creation ### Manual Object Creation
Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model, Sometimes statically defined fixtures don't suffice. This could be because of
or because the YAML format doesn't allow you to modify all of a model's state. the complexity of the tested model, or because the YAML format doesn't allow you
One common example here is publishing pages (page fixtures aren't published by default). to modify all of a model's state.
One common example here is publishing pages (page fixtures aren't published by
default).
You can always resort to creating objects manually in the test setup phase. You can always resort to creating objects manually in the test setup phase.
Since the test database is cleared on every test method, you'll get a fresh set of test instances every time.
Since the test database is cleared on every test method, you'll get a fresh set
of test instances every time.
:::php :::php
class SiteTreeTest extends SapphireTest { class SiteTreeTest extends SapphireTest {
function setUp() { function setUp() {
parent::setUp(); parent::setUp();
@ -140,16 +225,20 @@ Since the test database is cleared on every test method, you'll get a fresh set
### Why Factories? ### Why Factories?
While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention. While manually defined fixtures provide full flexibility, they offer very little
Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values, in terms of structure and convention. Alternatively, you can use the
callbacks on object creation, and dynamic/lazy value setting. `[api:FixtureFactory]` class, which allows you to set default values, callbacks
on object creation, and dynamic/lazy value setting.
<div class="hint" markdown='1'> <div class="hint" markdown='1'>
SapphireTest uses FixtureFactory under the hood when it is provided with YAML based fixtures. SapphireTest uses FixtureFactory under the hood when it is provided with YAML
based fixtures.
</div> </div>
The idea is that rather than instantiating objects directly, we'll have a factory class for them. The idea is that rather than instantiating objects directly, we'll have a
This factory can have so called "blueprints" defined on it, which tells the factory how to instantiate an object of a specific type. Blueprints need a name, which is usually set to the class it creates. factory class for them. This factory can have so called "blueprints" defined on
it, which tells the factory how to instantiate an object of a specific type.
Blueprints need a name, which is usually set to the class it creates.
### Usage ### Usage

View File

@ -12,6 +12,58 @@ class FixtureBlueprintTest extends SapphireTest {
'FixtureFactoryTest_DataObjectRelation' 'FixtureFactoryTest_DataObjectRelation'
); );
public function testCreateWithRelationshipExtraFields() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
$relation1 = new FixtureFactoryTest_DataObjectRelation();
$relation1->write();
$relation2 = new FixtureFactoryTest_DataObjectRelation();
$relation2->write();
// in YAML these look like
// RelationName:
// - =>Relational.obj:
// ExtraFieldName: test
// - =>..
$obj = $blueprint->createObject(
'one',
array(
'ManyMany' =>
array(
array(
"=>FixtureFactoryTest_DataObjectRelation.relation1" => array(),
"Label" => 'This is a label for relation 1'
),
array(
"=>FixtureFactoryTest_DataObjectRelation.relation2" => array(),
"Label" => 'This is a label for relation 2'
)
)
),
array(
'FixtureFactoryTest_DataObjectRelation' => array(
'relation1' => $relation1->ID,
'relation2' => $relation2->ID
)
)
);
$this->assertEquals(2, $obj->ManyMany()->Count());
$this->assertNotNull($obj->ManyMany()->find('ID', $relation1->ID));
$this->assertNotNull($obj->ManyMany()->find('ID', $relation2->ID));
$this->assertEquals(
array('Label' => 'This is a label for relation 1'),
$obj->ManyMany()->getExtraData('ManyMany', $relation1->ID)
);
$this->assertEquals(
array('Label' => 'This is a label for relation 2'),
$obj->ManyMany()->getExtraData('ManyMany', $relation2->ID)
);
}
public function testCreateWithoutData() { public function testCreateWithoutData() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
$obj = $blueprint->createObject('one'); $obj = $blueprint->createObject('one');
@ -28,6 +80,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->assertEquals('My Name', $obj->Name); $this->assertEquals('My Name', $obj->Name);
} }
public function testCreateWithRelationship() { public function testCreateWithRelationship() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
@ -127,7 +180,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->assertEquals(99, $obj->ID); $this->assertEquals(99, $obj->ID);
} }
function testCallbackOnBeforeCreate() { public function testCallbackOnBeforeCreate() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
$this->_called = 0; $this->_called = 0;
$self = $this; $self = $this;
@ -144,7 +197,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->_called = 0; $this->_called = 0;
} }
function testCallbackOnAfterCreate() { public function testCallbackOnAfterCreate() {
$blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject');
$this->_called = 0; $this->_called = 0;
$self = $this; $self = $this;
@ -161,7 +214,7 @@ class FixtureBlueprintTest extends SapphireTest {
$this->_called = 0; $this->_called = 0;
} }
function testDefineWithDefaultCustomSetters() { public function testDefineWithDefaultCustomSetters() {
$blueprint = new FixtureBlueprint( $blueprint = new FixtureBlueprint(
'FixtureFactoryTest_DataObject', 'FixtureFactoryTest_DataObject',
null, null,

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* @package framework * @package framework
* @subpackage tests * @subpackage tests
@ -151,19 +152,37 @@ class FixtureFactoryTest extends SapphireTest {
} }
/**
* @package framework
* @subpackage tests
*/
class FixtureFactoryTest_DataObject extends DataObject implements TestOnly { class FixtureFactoryTest_DataObject extends DataObject implements TestOnly {
private static $db = array( private static $db = array(
"Name" => "Varchar" "Name" => "Varchar"
); );
private static $many_many = array( private static $many_many = array(
"ManyMany" => "FixtureFactoryTest_DataObjectRelation" "ManyMany" => "FixtureFactoryTest_DataObjectRelation"
); );
private static $many_many_extraFields = array(
"ManyMany" => array(
"Label" => "Varchar"
)
);
} }
/**
* @package framework
* @subpackage tests
*/
class FixtureFactoryTest_DataObjectRelation extends DataObject implements TestOnly { class FixtureFactoryTest_DataObjectRelation extends DataObject implements TestOnly {
private static $db = array( private static $db = array(
"Name" => "Varchar" "Name" => "Varchar"
); );
private static $belongs_many_many = array( private static $belongs_many_many = array(
"TestParent" => "FixtureFactoryTest_DataObject" "TestParent" => "FixtureFactoryTest_DataObject"
); );