1 <?php
2
3 require_once 'thirdparty/spyc/spyc.php';
4
5 /**
6 * Uses the Spyc library to parse a YAML document (see http://yaml.org).
7 * YAML is a simple markup languages that uses tabs and colons instead of the more verbose XML tags,
8 * and because of this much better for developers creating files by hand.
9 *
10 * The contents of the YAML file are broken into three levels:
11 * - Top level: class names - Page and ErrorPage. This is the name of the dataobject class that should be created.
12 * The fact that ErrorPage is actually a subclass is irrelevant to the system populating the database.
13 * Each identifier you specify delimits a new database record.
14 * This means that every record needs to have an identifier, whether you use it or not.
15 * - Third level: fields - each field for the record is listed as a 3rd level entry.
16 * In most cases, the fieldŐs raw content is provided.
17 * However, if you want to define a relationship, you can do so using "=>"
18 *
19 * There are a couple of lines like this:
20 * <code>
21 * Parent: =>Page.about
22 * </code>
23 * This will tell the system to set the ParentID database field to the ID of the Page object with the identifier ŇaboutÓ.
24 * This can be used on any has-one or many-many relationship.
25 * Note that we use the name of the relationship (Parent), and not the name of the database field (ParentID)
26 *
27 * On many-many relationships, you should specify a comma separated list of values.
28 * <code>
29 * MyRelation: =>Class.inst1,=>Class.inst2,=>Class.inst3
30 * </code>
31 * An crucial thing to note is that the YAML file specifies DataObjects, not database records.
32 * The database is populated by instantiating DataObject objects, setting the fields listed, and calling write().
33 * This means that any onBeforeWrite() or default value logic will be executed as part of the test.
34 * This forms the basis of our testURLGeneration() test above.
35 *
36 * For example, the URLSegment value of Page.staffduplicate is the same as the URLSegment value of Page.staff.
37 * When the fixture is set up, the URLSegment value of Page.staffduplicate will actually be my-staff-2.
38 *
39 * Finally, be aware that requireDefaultRecords() is not called by the database populator -
40 * so you will need to specify standard pages such as 404 and home in your YAML file.
41 *
42 * <code>
43 * Page:
44 * home:
45 * Title: Home
46 * about:
47 * Title: About Us
48 * staff:
49 * Title: Staff
50 * URLSegment: my-staff
51 * Parent: =>Page.about
52 * staffduplicate:
53 * Title: Staff
54 * URLSegment: my-staff
55 * Parent: =>Page.about
56 * products:
57 * Title: Products
58 * ErrorPage:
59 * 404:
60 * Title: Page not Found
61 * ErrorCode: 404
62 * </code>
63 *
64 * @package sapphire
65 * @subpackage core
66 *
67 * @see http://spyc.sourceforge.net/
68 *
69 * @todo Write unit test for YamlFixture
70 *
71 * @param $fixtureFile The location of the .yml fixture file, relative to the site base dir
72 */
73 class YamlFixture extends Object {
74
75 /**
76 * The location of the .yml fixture file, relative to the site base dir
77 *
78 * @var string
79 */
80 protected $fixtureFile;
81
82 /**
83 * Array of fixture items
84 *
85 * @var array
86 */
87 protected $fixtureDictionary;
88
89 function __construct($fixtureFile) {
90 if(!file_exists(Director::baseFolder().'/'. $fixtureFile)) {
91 user_error('YamlFixture::__construct(): Fixture path "' . $fixtureFile . '" not found', E_USER_ERROR);
92 }
93
94 $this->fixtureFile = $fixtureFile;
95 parent::__construct();
96 }
97
98 /**
99 * Get the ID of an object from the fixture.
100 * @param $className The data class, as specified in your fixture file. Parent classes won't work
101 * @param $identifier The identifier string, as provided in your fixture file
102 */
103 public function idFromFixture($className, $identifier) {
104 if(isset($this->fixtureDictionary[$className][$identifier])) {
105 return $this->fixtureDictionary[$className][$identifier];
106 } else {
107 return false;
108 }
109
110 }
111
112 /**
113 * Return all of the IDs in the fixture of a particular class name.
114 *
115 * @return A map of fixture-identifier => object-id
116 */
117 public function allFixtureIDs($className) {
118 if(isset($this->fixtureDictionary[$className])) {
119 return $this->fixtureDictionary[$className];
120 } else {
121 return false;
122 }
123
124 }
125
126 /**
127 * Get an object from the fixture.
128 *
129 * @param $className The data class, as specified in your fixture file. Parent classes won't work
130 * @param $identifier The identifier string, as provided in your fixture file
131 */
132 public function objFromFixture($className, $identifier) {
133 $id = $this->idFromFixture($className, $identifier);
134 if($id) return DataObject::get_by_id($className, $id);
135 }
136
137 /**
138 * Load a YAML fixture file into the database.
139 * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
140 *
141 * Caution: In order to support reflexive relations which need a valid object ID,
142 * the record is written twice: first after populating all non-relational fields,
143 * then again after populating all relations (has_one, has_many, many_many).
144 */
145 public function saveIntoDatabase() {
146 // We have to disable validation while we import the fixtures, as the order in
147 // which they are imported doesnt guarantee valid relations until after the
148 // import is complete.
149 $validationenabled = DataObject::get_validation_enabled();
150 DataObject::set_validation_enabled(false);
151
152 $parser = new Spyc();
153 $fixtureContent = $parser->loadFile(Director::baseFolder().'/'.$this->fixtureFile);
154
155 $this->fixtureDictionary = array();
156 foreach($fixtureContent as $dataClass => $items) {
157 if(ClassInfo::exists($dataClass)) {
158 $this->writeDataObject($dataClass, $items);
159 } else {
160 $this->writeSQL($dataClass, $items);
161 }
162 }
163
164 DataObject::set_validation_enabled($validationenabled);
165 }
166
167 /**
168 * Writes the fixture into the database using DataObjects
169 *
170 * @param string $dataClass
171 * @param array $items
172 */
173 protected function writeDataObject($dataClass, $items) {
174 foreach($items as $identifier => $fields) {
175 $obj = new $dataClass();
176
177 // If an ID is explicitly passed, then we'll sort out the initial write straight away
178 // This is just in case field setters triggered by the population code in the next block
179 // Call $this->write(). (For example, in FileTest)
180 if(isset($fields['ID'])) {
181 $obj->ID = $fields['ID'];
182
183 // The database needs to allow inserting values into the foreign key column (ID in our case)
184 $conn = DB::getConn();
185 if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($dataClass), true);
186 $obj->write(false, true);
187 if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($dataClass), false);
188 }
189
190 // Populate the dictionary with the ID
191 if($fields) foreach($fields as $fieldName => $fieldVal) {
192 if($obj->many_many($fieldName) || $obj->has_many($fieldName) || $obj->has_one($fieldName)) continue;
193 $obj->$fieldName = $this->parseFixtureVal($fieldVal);
194 }
195 $obj->write();
196
197 // has to happen before relations in case a class is referring to itself
198 $this->fixtureDictionary[$dataClass][$identifier] = $obj->ID;
199
200 // Populate all relations
201 if($fields) foreach($fields as $fieldName => $fieldVal) {
202 if($obj->many_many($fieldName) || $obj->has_many($fieldName)) {
203 $parsedItems = array();
204 $items = preg_split('/ *, */',trim($fieldVal));
205 foreach($items as $item) {
206 $parsedItems[] = $this->parseFixtureVal($item);
207 }
208 $obj->write();
209 if($obj->has_many($fieldName)) {
210 $obj->getComponents($fieldName)->setByIDList($parsedItems);
211 } elseif($obj->many_many($fieldName)) {
212 $obj->getManyManyComponents($fieldName)->setByIDList($parsedItems);
213 }
214 } elseif($obj->has_one($fieldName)) {
215 $obj->{$fieldName . 'ID'} = $this->parseFixtureVal($fieldVal);
216 }
217 }
218 $obj->write();
219 }
220 }
221
222 /**
223 * Writes the fixture into the database directly using a database manipulation
224 *
225 * @param string $table
226 * @param array $items
227 */
228 protected function writeSQL($table, $items) {
229 foreach($items as $identifier => $fields) {
230 $manipulation = array($table => array("fields" => array(), "command" => "insert"));
231 foreach($fields as $fieldName=> $fieldVal) {
232 $manipulation[$table]["fields"][$fieldName] = "'".$this->parseFixtureVal($fieldVal)."'";
233 }
234 DB::manipulate($manipulation);
235 $this->fixtureDictionary[$table][$identifier] = DB::getGeneratedID($table);
236 }
237 }
238
239 /**
240 * Parse a value from a fixture file. If it starts with => it will get an ID from the fixture dictionary
241 */
242 protected function parseFixtureVal($fieldVal) {
243 // Parse a dictionary reference - used to set foreign keys
244 if(substr($fieldVal,0,2) == '=>') {
245 list($a, $b) = explode('.', substr($fieldVal,2), 2);
246 return $this->fixtureDictionary[$a][$b];
247
248 // Regular field value setting
249 } else {
250 return $fieldVal;
251 }
252 }
253 }
254