From 8b923006227b0177983c96b949edaa6df18fbbf8 Mon Sep 17 00:00:00 2001 From: Will Rossiter Date: Mon, 24 Feb 2014 21:41:48 +1300 Subject: [PATCH] Add support for many_many_extraField in YAML Format is RelationName: - =>Obj.name: ExtraFieldName: "Foo" --- dev/FixtureBlueprint.php | 57 +++++++--- docs/en/topics/testing/fixtures.md | 177 ++++++++++++++++++++++------- tests/dev/FixtureBlueprintTest.php | 59 +++++++++- tests/dev/FixtureFactoryTest.php | 19 ++++ 4 files changed, 247 insertions(+), 65 deletions(-) diff --git a/dev/FixtureBlueprint.php b/dev/FixtureBlueprint.php index e74bdee11..532c16b56 100644 --- a/dev/FixtureBlueprint.php +++ b/dev/FixtureBlueprint.php @@ -128,27 +128,48 @@ class FixtureBlueprint { // Populate all relations if($data) foreach($data as $fieldName => $fieldVal) { if($obj->many_many($fieldName) || $obj->has_many($fieldName)) { + $obj->write(); + $parsedItems = array(); - $items = preg_split('/ *, */',trim($fieldVal)); - foreach($items as $item) { - // Check for correct format: =>.. - // Ignore if the item has already been replaced with a numeric DB identifier - if(!is_numeric($item) && !preg_match('/^=>[^\.]+\.[^\.]+/', $item)) { - throw new InvalidArgumentException(sprintf( - 'Invalid format for relation "%s" on class "%s" ("%s")', - $fieldName, - $class, - $item - )); + + 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)); + + foreach($items as $item) { + // Check for correct format: =>.. + // Ignore if the item has already been replaced with a numeric DB identifier + if(!is_numeric($item) && !preg_match('/^=>[^\.]+\.[^\.]+/', $item)) { + throw new InvalidArgumentException(sprintf( + 'Invalid format for relation "%s" on class "%s" ("%s")', + $fieldName, + $class, + $item + )); + } + + $parsedItems[] = $this->parseValue($item, $fixtures); } - $parsedItems[] = $this->parseValue($item, $fixtures); - } - $obj->write(); - if($obj->has_many($fieldName)) { - $obj->getComponents($fieldName)->setByIDList($parsedItems); - } elseif($obj->many_many($fieldName)) { - $obj->getManyManyComponents($fieldName)->setByIDList($parsedItems); + if($obj->has_many($fieldName)) { + $obj->getComponents($fieldName)->setByIDList($parsedItems); + } elseif($obj->many_many($fieldName)) { + $obj->getManyManyComponents($fieldName)->setByIDList($parsedItems); + } } } elseif($obj->has_one($fieldName)) { // Sets has_one with relation name diff --git a/docs/en/topics/testing/fixtures.md b/docs/en/topics/testing/fixtures.md index 77a1886c2..5053cd143 100644 --- a/docs/en/topics/testing/fixtures.md +++ b/docs/en/topics/testing/fixtures.md @@ -2,38 +2,42 @@ ## Overview -You will often find the need to test your functionality with some consistent data. -If we are testing our code with the same data each time, -we can trust our tests to yeild 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 - -all we have to do is define them, and we have a few ways in which we can do this. +You will often find the need to test your functionality with some consistent +data. If we are testing our code with the same data each time, we can trust our +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 - all we have to do is define them, and +we have a few ways in which we can do this. ## YAML Fixtures -YAML is a markup language which is deliberately simple and easy to read, -so it is ideal for fixture generation. +YAML is a markup language which is deliberately simple and easy to read, so it +is ideal for fixture generation. Say we have the following two DataObjects: :::php class Player extends DataObject { - static $db = array ( + + private static $db = array ( 'Name' => 'Varchar(255)' ); - static $has_one = array( + private static $has_one = array( 'Team' => 'Team' ); } class Team extends DataObject { - static $db = array ( + + private static $db = array ( 'Name' => 'Varchar(255)', 'Origin' => 'Varchar(255)' ); - static $has_many = array( + private static $has_many = array( 'Players' => 'Player' ); } @@ -59,31 +63,42 @@ We can represent multiple instances of them in `YAML` as follows: Name: The Crusaders Origin: Bay of Plenty -Our `YAML` is broken up into three levels, signified by the indentation of each line. -In the first level of indentation, `Player` and `Team`, -represent the class names of the objects we want to be created for the test. +Our `YAML` is broken up into three levels, signified by the indentation of each +line. In the first level of indentation, `Player` and `Team`, represent the +class names of the objects we want to be created for the test. -The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are identifiers. -These are what you pass as the second argument of `SapphireTest::objFromFixture()`. -Each identifier you specify represents a new object. +The second level, `john`/`joe`/`jack` & `hurricanes`/`crusaders`, are +identifiers. These are what you pass as the second argument of +`SapphireTest::objFromFixture()`. Each identifier you specify represents a new +object. 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, -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`. +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, 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`, just like how a relationship is always set up.
-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).
-This style of relationship declaration can be used for both a `has-one` and a `many-many` relationship. -For `many-many` relationships, we specify a comma separated list of values. +This style of relationship declaration can be used for both a `has-one` and a +`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: :::yml @@ -104,27 +119,97 @@ For example we could just as easily write the above as: Origin: Bay of Plenty Players: =>Player.joe,=>Player.jack -A crucial thing to note is that **the YAML file specifies DataObjects, not database records**. -The database is populated by instantiating DataObject objects and setting the fields declared in the YML, -then calling write() on those objects. -This means that any `onBeforeWrite()` or default value logic will be executed as part of 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). +A crucial thing to note is that **the YAML file specifies DataObjects, not +database records**. + +The database is populated by instantiating DataObject objects and setting the +fields declared in the YML, then calling write() on those objects. This means +that any `onBeforeWrite()` or default value logic will be executed as part of +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 ### Manual Object Creation -Sometimes statically defined fixtures don't suffice. This could be because of the complexity of the tested model, -or because the YAML format doesn't allow you to modify all of a model's state. -One common example here is publishing pages (page fixtures aren't published by default). +Sometimes statically defined fixtures don't suffice. This could be because of +the complexity of the tested model, or because the YAML format doesn't allow you +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. -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 class SiteTreeTest extends SapphireTest { + function 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? -While manually defined fixtures provide full flexibility, they offer very little in terms of structure and convention. -Alternatively, you can use the `[api:FixtureFactory]` class, which allows you to set default values, -callbacks on object creation, and dynamic/lazy value setting. +While manually defined fixtures provide full flexibility, they offer very little +in terms of structure and convention. Alternatively, you can use the +`[api:FixtureFactory]` class, which allows you to set default values, callbacks +on object creation, and dynamic/lazy value setting.
-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.
-The idea is that rather than instantiating objects directly, we'll have a 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. +The idea is that rather than instantiating objects directly, we'll have a +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 diff --git a/tests/dev/FixtureBlueprintTest.php b/tests/dev/FixtureBlueprintTest.php index a5613d85f..591ab6e36 100644 --- a/tests/dev/FixtureBlueprintTest.php +++ b/tests/dev/FixtureBlueprintTest.php @@ -12,6 +12,58 @@ class FixtureBlueprintTest extends SapphireTest { '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() { $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); $obj = $blueprint->createObject('one'); @@ -28,6 +80,7 @@ class FixtureBlueprintTest extends SapphireTest { $this->assertEquals('My Name', $obj->Name); } + public function testCreateWithRelationship() { $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); @@ -127,7 +180,7 @@ class FixtureBlueprintTest extends SapphireTest { $this->assertEquals(99, $obj->ID); } - function testCallbackOnBeforeCreate() { + public function testCallbackOnBeforeCreate() { $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); $this->_called = 0; $self = $this; @@ -144,7 +197,7 @@ class FixtureBlueprintTest extends SapphireTest { $this->_called = 0; } - function testCallbackOnAfterCreate() { + public function testCallbackOnAfterCreate() { $blueprint = new FixtureBlueprint('FixtureFactoryTest_DataObject'); $this->_called = 0; $self = $this; @@ -161,7 +214,7 @@ class FixtureBlueprintTest extends SapphireTest { $this->_called = 0; } - function testDefineWithDefaultCustomSetters() { + public function testDefineWithDefaultCustomSetters() { $blueprint = new FixtureBlueprint( 'FixtureFactoryTest_DataObject', null, diff --git a/tests/dev/FixtureFactoryTest.php b/tests/dev/FixtureFactoryTest.php index 2dd7685e2..a08d87ef7 100644 --- a/tests/dev/FixtureFactoryTest.php +++ b/tests/dev/FixtureFactoryTest.php @@ -1,4 +1,5 @@ "Varchar" ); + private static $many_many = array( "ManyMany" => "FixtureFactoryTest_DataObjectRelation" ); + + private static $many_many_extraFields = array( + "ManyMany" => array( + "Label" => "Varchar" + ) + ); } +/** + * @package framework + * @subpackage tests + */ class FixtureFactoryTest_DataObjectRelation extends DataObject implements TestOnly { + private static $db = array( "Name" => "Varchar" ); + private static $belongs_many_many = array( "TestParent" => "FixtureFactoryTest_DataObject" );