Version: current

      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

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -