AssetManager   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 21
lcom 1
cbo 3
dl 0
loc 245
ccs 75
cts 75
cp 1
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A add() 0 4 1
A inject() 0 4 1
A populate() 0 16 3
A pepper() 0 18 3
B createPackages() 0 36 4
A createPackage() 0 14 3
B sortPackages() 0 31 3
A pepperPackages() 0 14 3
1
<?php
2
3
namespace mindplay\implant;
4
5
use Closure;
6
use ReflectionFunction;
7
use ReflectionParameter;
8
use UnexpectedValueException;
9
use MJS\TopSort\Implementations\StringSort;
10
11
/**
12
 * This class manages {@see AssetPackage} creation and orders them correctly according to
13
 * their dependencies on other packages.
14
 */
15
class AssetManager
16
{
17
    /**
18
     * @var null[] map where asset package class-name => NULL
19
     */
20
    protected $class_names = [];
21
22
    /**
23
     * @var AssetInjection[] list of injected, anonymous asset packages
24
     */
25
    protected $injections = [];
26
27
    /**
28
     * @var Closure[] list of callbacks for peppering packages
29
     */
30
    protected $peppering = [];
31
32
    /**
33
     * Add a given package, including (recursively) the dependencies of that package.
34
     *
35
     * Adding the same package more than once has no effect.
36
     *
37
     * The order in which packages are added also has no effect.
38
     *
39
     * @param string $class_name fully-qualified class-name of asset package class
40
     *
41
     * @return void
42
     */
43 1
    public function add($class_name)
44
    {
45 1
        $this->class_names[$class_name] = null;
46 1
    }
47
48
    /**
49
     * Directly inject an anonymous asset package.
50
     *
51
     * This lends itself well to things like initialization scripts in controllers or views.
52
     *
53
     * Internally, this is treated just a class-based package, in terms of sorting the injected
54
     * package according to it's dependencies; the only difference is, you can't refer to an
55
     * injected package as a dependency anywhere else.
56
     *
57
     * The `$callback` is similar to the {@see AssetPackage::defineAssets()} method, and
58
     * `$dependencies` is similar to {@see AssetPackage::getDependencies()}.
59
     *
60
     * @param callable $callback     asset definition callback - function ($model) : void
61
     * @param string[] $dependencies list of fully-qualified class-names of package dependencies
62
     *
63
     * @return void
64
     */
65 1
    public function inject(callable $callback, array $dependencies = [])
66
    {
67 1
        $this->injections[] = new AssetInjection($callback, $dependencies);
68 1
    }
69
70
    /**
71
     * Populate the given asset model by creating and sorting packages, and then
72
     * applying {@see AssetPackage::defineAssets()} of every package to the given model.
73
     *
74
     * @param object $model asset model
75
     *
76
     * @return void
77
     */
78 1
    public function populate($model)
79
    {
80 1
        $packages = $this->createPackages();
81
82 1
        if (empty($packages)) {
83 1
            return; // no packages added
84
        }
85
86 1
        $packages = $this->sortPackages($packages);
87
88 1
        $this->pepperPackages($packages);
89
90 1
        foreach ($packages as $package) {
91 1
            $package->defineAssets($model);
92 1
        }
93 1
    }
94
95
    /**
96
     * Pepper a created AssetPackage using a callback function - this will be called
97
     * when you {@see populate()} your asset model, before calling the
98
     * {@see AssetPackage::defineAssets()} functions of every added package.
99
     *
100
     * The given function must accept precisely one argument (and should return nothing)
101
     * and must be type-hinted to specify which package you wish to pepper.
102
     *
103
     * @param Closure $callback function (PackageType $package) : void
104
     *
105
     * @return void
106
     *
107
     * @throws UnexpectedValueException if the given function does not accept precisely one argument
108
     */
109 1
    public function pepper($callback)
110
    {
111 1
        $function = new ReflectionFunction($callback);
112
113 1
        $params = $function->getParameters();
114
115 1
        if (count($params) !== 1 || $params[0]->getClass() === null) {
116 1
            $file = $function->getFileName();
117 1
            $line = $function->getStartLine();
118
119 1
            throw new UnexpectedValueException(
120 1
                "unexpected function signature at: {$file}, line {$line} " .
121
                "(pepper functions must accept precisely one argument, and must provide a type-hint)"
122 1
            );
123
        }
124
125 1
        $this->peppering[] = $callback;
126 1
    }
127
128
    /**
129
     * Create all packages
130
     *
131
     * @return AssetPackage[] map of asset packages
132
     */
133 1
    private function createPackages()
134
    {
135
        /**
136
         * @var AssetPackage[] $packages
137
         * @var AssetPackage[] $pending
138
         */
139
140 1
        $class_names = array_keys($this->class_names);
141
142 1
        $packages = array_merge(
143 1
            $this->injections,
144 1
            array_combine($class_names, array_map([$this, "createPackage"], $class_names))
145 1
        );
146
147 1
        $pending = array_keys($packages);
148
149 1
        $done = [];
150
151 1
        while (count($pending)) {
152 1
            $index = array_pop($pending);
153
154 1
            if (isset($done[$index])) {
155 1
                continue;
156
            }
157
158 1
            if (!isset($packages[$index])) {
159 1
                $packages[$index] = $this->createPackage($index);
160 1
            }
161
162 1
            $pending = array_merge($pending, $packages[$index]->listDependencies());
163
164 1
            $done[$index] = true;
165 1
        }
166
167 1
        return $packages;
168
    }
169
170
    /**
171
     * Create an individual package
172
     *
173
     * @param string $class_name package class-name
174
     *
175
     * @return AssetPackage
176
     *
177
     * @throws UnexpectedValueException
178
     */
179 1
    protected function createPackage($class_name)
180
    {
181 1
        if (!class_exists($class_name)) {
182 1
            throw new UnexpectedValueException("undefined package class: {$class_name}");
183
        }
184
185 1
        $package = new $class_name();
186
187 1
        if (!$package instanceof AssetPackage) {
188 1
            throw new UnexpectedValueException("class must implement the AssetPackage interface: {$class_name}");
189
        }
190
191 1
        return $package;
192
    }
193
194
    /**
195
     * Internally sort packages in dependency ("topological") order.
196
     *
197
     * Packages are initially sorted by name, to guarantee a predictable base order, e.g.
198
     * unaffected by the order in which the packages were added.
199
     *
200
     * @param AssetPackage[] $packages map of packages
201
     *
202
     * @return AssetPackage[] sorted map of packages
203
     */
204 1
    private function sortPackages($packages)
205
    {
206
        /**
207
         * @var string[]       $order  list of topologically sorted class-names
208
         * @var AssetPackage[] $sorted resulting ordered list of asset packages
209
         */
210
211
        // pre-sort packages by index:
212
213 1
        ksort($packages, SORT_STRING);
214
215
        // topologically sort packages by dependencies:
216
217 1
        $sorter = new StringSort();
218
219 1
        foreach ($packages as $index => $package) {
220 1
            $sorter->add($index, $package->listDependencies());
221 1
        }
222
223 1
        $order = $sorter->sort(); // TODO QA: catch and re-throw CircularDependencyException here?
224
225
        // create sorted map of packages:
226
227 1
        $sorted = [];
228
229 1
        foreach ($order as $index) {
230 1
            $sorted[$index] = $packages[$index];
231 1
        }
232
233 1
        return $sorted;
234
    }
235
236
    /**
237
     * Expose packages to previously added peppering functions.
238
     *
239
     * @param AssetPackage[] $packages
240
     *
241
     * @return void
242
     *
243
     * @see pepper()
244
     */
245 1
    private function pepperPackages($packages)
246
    {
247 1
        foreach ($this->peppering as $pepper) {
248 1
            $param = new ReflectionParameter($pepper, 0);
249
250 1
            $class = $param->getClass();
251
252 1
            $name = $class->name;
253
254 1
            if (isset($packages[$name])) {
255 1
                call_user_func($pepper, $packages[$name]);
256 1
            }
257 1
        }
258 1
    }
259
}
260