Completed
Push — authenticator-refactor ( 16f104...61b037 )
by Simon
06:52
created

FixtureFactory::get()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 2
dl 0
loc 19
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\ORM\Queries\SQLInsert;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\ORM\Queries\SQLDelete;
9
use SilverStripe\Core\Injector\Injector;
10
use InvalidArgumentException;
11
12
/**
13
 * Manages a set of database fixtures for {@link DataObject} records
14
 * as well as raw database table rows.
15
 *
16
 * Delegates creation of objects to {@link FixtureBlueprint},
17
 * which can implement class- and use-case specific fixture setup.
18
 *
19
 * Supports referencing model relations through a specialized syntax:
20
 * <code>
21
 * $factory = new FixtureFactory();
22
 * $relatedObj = $factory->createObject(
23
 *  'MyRelatedClass',
24
 *  'relation1'
25
 * );
26
 * $obj = $factory->createObject(
27
 *  'MyClass',
28
 *  'object1'
29
 *  array('MyRelationName' => '=>MyRelatedClass.relation1')
30
 * );
31
 * </code>
32
 * Relation loading is order dependant.
33
 */
34
class FixtureFactory
35
{
36
37
    /**
38
     * @var array Array of fixture items, keyed by class and unique identifier,
39
     * with values being the generated database ID. Does not store object instances.
40
     */
41
    protected $fixtures = array();
42
43
    /**
44
     * @var array Callbacks
45
     */
46
    protected $blueprints = array();
47
48
    /**
49
     * @param string $name Unique name for this blueprint
50
     * @param array|FixtureBlueprint $defaults Array of default values, or a blueprint instance
51
     * @return $this
52
     */
53
    public function define($name, $defaults = array())
54
    {
55
        if ($defaults instanceof FixtureBlueprint) {
56
            $this->blueprints[$name] = $defaults;
57
        } else {
58
            $class = $name;
59
            $this->blueprints[$name] = Injector::inst()->create(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method create() does only exist in the following implementations of said interface: SilverStripe\Core\Injector\Injector.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
60
                'SilverStripe\\Dev\\FixtureBlueprint',
61
                $name,
62
                $class,
63
                $defaults
64
            );
65
        }
66
67
        return $this;
68
    }
69
70
    /**
71
     * Writes the fixture into the database using DataObjects
72
     *
73
     * @param string $name Name of the {@link FixtureBlueprint} to use,
74
     *                     usually a DataObject subclass.
75
     * @param string $identifier Unique identifier for this fixture type
76
     * @param array $data Map of properties. Overrides default data.
77
     * @return DataObject
78
     */
79
    public function createObject($name, $identifier, $data = null)
80
    {
81
        if (!isset($this->blueprints[$name])) {
82
            $this->blueprints[$name] = new FixtureBlueprint($name);
83
        }
84
        $blueprint = $this->blueprints[$name];
85
        $obj = $blueprint->createObject($identifier, $data, $this->fixtures);
86
        $class = $blueprint->getClass();
87
88
        if (!isset($this->fixtures[$class])) {
89
            $this->fixtures[$class] = array();
90
        }
91
        $this->fixtures[$class][$identifier] = $obj->ID;
92
93
        return $obj;
94
    }
95
96
    /**
97
     * Writes the fixture into the database directly using a database manipulation.
98
     * Does not use blueprints. Only supports tables with a primary key.
99
     *
100
     * @param string $table Existing database table name
101
     * @param string $identifier Unique identifier for this fixture type
102
     * @param array $data Map of properties
103
     * @return int Database identifier
104
     */
105
    public function createRaw($table, $identifier, $data)
106
    {
107
        $fields = array();
108
        foreach ($data as $fieldName => $fieldVal) {
109
            $fields["\"{$fieldName}\""] = $this->parseValue($fieldVal);
110
        }
111
        $insert = new SQLInsert("\"{$table}\"", $fields);
112
        $insert->execute();
113
        $id = DB::get_generated_id($table);
114
        $this->fixtures[$table][$identifier] = $id;
115
116
        return $id;
117
    }
118
119
    /**
120
     * Get the ID of an object from the fixture.
121
     *
122
     * @param string $class The data class, as specified in your fixture file.  Parent classes won't work
123
     * @param string $identifier The identifier string, as provided in your fixture file
124
     * @return int
125
     */
126
    public function getId($class, $identifier)
127
    {
128
        if (isset($this->fixtures[$class][$identifier])) {
129
            return $this->fixtures[$class][$identifier];
130
        } else {
131
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by SilverStripe\Dev\FixtureFactory::getId of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
132
        }
133
    }
134
135
    /**
136
     * Return all of the IDs in the fixture of a particular class name.
137
     *
138
     * @param string $class The data class or table name
139
     * @return array|false A map of fixture-identifier => object-id
140
     */
141
    public function getIds($class)
142
    {
143
        if (isset($this->fixtures[$class])) {
144
            return $this->fixtures[$class];
145
        } else {
146
            return false;
147
        }
148
    }
149
150
    /**
151
     * @param string $class
152
     * @param string $identifier
153
     * @param int $databaseId
154
     * @return $this
155
     */
156
    public function setId($class, $identifier, $databaseId)
157
    {
158
        $this->fixtures[$class][$identifier] = $databaseId;
159
        return $this;
160
    }
161
162
    /**
163
     * Get an object from the fixture.
164
     *
165
     * @param string $class The data class or table name, as specified in your fixture file.  Parent classes won't work
166
     * @param string $identifier The identifier string, as provided in your fixture file
167
     * @return DataObject
168
     */
169
    public function get($class, $identifier)
170
    {
171
        $id = $this->getId($class, $identifier);
172
        if (!$id) {
173
            return null;
174
        }
175
176
        // If the class doesn't exist, look for a table instead
177
        if (!class_exists($class)) {
178
            $tableNames = DataObject::getSchema()->getTableNames();
179
            $potential = array_search($class, $tableNames);
180
            if (!$potential) {
181
                throw new \LogicException("'$class' is neither a class nor a table name");
182
            }
183
            $class = $potential;
184
        }
185
186
        return DataObject::get_by_id($class, $id);
187
    }
188
189
    /**
190
     * @return array Map of class names, containing a map of in-memory identifiers
191
     * mapped to database identifiers.
192
     */
193
    public function getFixtures()
194
    {
195
        return $this->fixtures;
196
    }
197
198
    /**
199
     * Remove all fixtures previously defined through {@link createObject()}
200
     * or {@link createRaw()}, both from the internal fixture mapping and the database.
201
     * If the $class argument is set, limit clearing to items of this class.
202
     *
203
     * @param string $limitToClass
204
     */
205
    public function clear($limitToClass = null)
206
    {
207
        $classes = ($limitToClass) ? array($limitToClass) : array_keys($this->fixtures);
208
        foreach ($classes as $class) {
209
            $ids = $this->fixtures[$class];
210
            foreach ($ids as $id => $dbId) {
211
                if (class_exists($class)) {
212
                    $class::get()->byId($dbId)->delete();
213
                } else {
214
                    $table = $class;
215
                    $delete = new SQLDelete("\"$table\"", array(
216
                        "\"$table\".\"ID\"" => $dbId
217
                    ));
218
                    $delete->execute();
219
                }
220
221
                unset($this->fixtures[$class][$id]);
222
            }
223
        }
224
    }
225
226
    /**
227
     * @return array Of {@link FixtureBlueprint} instances
228
     */
229
    public function getBlueprints()
230
    {
231
        return $this->blueprints;
232
    }
233
234
    /**
235
     * @param String $name
236
     * @return FixtureBlueprint|false
237
     */
238
    public function getBlueprint($name)
239
    {
240
        return (isset($this->blueprints[$name])) ? $this->blueprints[$name] : false;
241
    }
242
243
    /**
244
     * Parse a value from a fixture file.  If it starts with =>
245
     * it will get an ID from the fixture dictionary
246
     *
247
     * @param string $value
248
     * @return string Fixture database ID, or the original value
249
     */
250
    protected function parseValue($value)
251
    {
252
        if (substr($value, 0, 2) == '=>') {
253
            // Parse a dictionary reference - used to set foreign keys
254
            if (strpos($value, '.') !== false) {
255
                list($class, $identifier) = explode('.', substr($value, 2), 2);
256
            } else {
257
                throw new \LogicException("Bad fixture lookup identifier: " . $value);
258
            }
259
260
            if ($this->fixtures && !isset($this->fixtures[$class][$identifier])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fixtures of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
261
                throw new InvalidArgumentException(sprintf(
262
                    'No fixture definitions found for "%s"',
263
                    $value
264
                ));
265
            }
266
267
            return $this->fixtures[$class][$identifier];
268
        } else {
269
            // Regular field value setting
270
            return $value;
271
        }
272
    }
273
}
274