1<?php
2
3/*
4 * This file is part of the Assetic package, an OpenSky project.
5 *
6 * (c) 2010-2014 OpenSky Project Inc
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Assetic\Factory\Loader;
13
14use Assetic\Factory\AssetFactory;
15use Assetic\Factory\Resource\ResourceInterface;
16use Assetic\Util\FilesystemUtils;
17
18/**
19 * Loads asset formulae from PHP files.
20 *
21 * @author Kris Wallsmith <kris.wallsmith@gmail.com>
22 */
23abstract class BasePhpFormulaLoader implements FormulaLoaderInterface
24{
25    protected $factory;
26    protected $prototypes;
27
28    public function __construct(AssetFactory $factory)
29    {
30        $this->factory = $factory;
31        $this->prototypes = array();
32
33        foreach ($this->registerPrototypes() as $prototype => $options) {
34            $this->addPrototype($prototype, $options);
35        }
36    }
37
38    public function addPrototype($prototype, array $options = array())
39    {
40        $tokens = token_get_all('<?php '.$prototype);
41        array_shift($tokens);
42
43        $this->prototypes[$prototype] = array($tokens, $options);
44    }
45
46    public function load(ResourceInterface $resource)
47    {
48        if (!$nbProtos = count($this->prototypes)) {
49            throw new \LogicException('There are no prototypes registered.');
50        }
51
52        $buffers = array_fill(0, $nbProtos, '');
53        $bufferLevels = array_fill(0, $nbProtos, 0);
54        $buffersInWildcard = array();
55
56        $tokens = token_get_all($resource->getContent());
57        $calls = array();
58
59        while ($token = array_shift($tokens)) {
60            $current = self::tokenToString($token);
61            // loop through each prototype (by reference)
62            foreach (array_keys($this->prototypes) as $i) {
63                $prototype = & $this->prototypes[$i][0];
64                $options = $this->prototypes[$i][1];
65                $buffer = & $buffers[$i];
66                $level = & $bufferLevels[$i];
67
68                if (isset($buffersInWildcard[$i])) {
69                    switch ($current) {
70                        case '(': ++$level; break;
71                        case ')': --$level; break;
72                    }
73
74                    $buffer .= $current;
75
76                    if (!$level) {
77                        $calls[] = array($buffer.';', $options);
78                        $buffer = '';
79                        unset($buffersInWildcard[$i]);
80                    }
81                } elseif ($current == self::tokenToString(current($prototype))) {
82                    $buffer .= $current;
83                    if ('*' == self::tokenToString(next($prototype))) {
84                        $buffersInWildcard[$i] = true;
85                        ++$level;
86                    }
87                } else {
88                    reset($prototype);
89                    unset($buffersInWildcard[$i]);
90                    $buffer = '';
91                }
92            }
93        }
94
95        $formulae = array();
96        foreach ($calls as $call) {
97            $formulae += call_user_func_array(array($this, 'processCall'), $call);
98        }
99
100        return $formulae;
101    }
102
103    private function processCall($call, array $protoOptions = array())
104    {
105        $tmp = FilesystemUtils::createTemporaryFile('php_formula_loader');
106        file_put_contents($tmp, implode("\n", array(
107            '<?php',
108            $this->registerSetupCode(),
109            $call,
110            'echo serialize($_call);',
111        )));
112        $args = unserialize(shell_exec('php '.escapeshellarg($tmp)));
113        unlink($tmp);
114
115        $inputs  = isset($args[0]) ? self::argumentToArray($args[0]) : array();
116        $filters = isset($args[1]) ? self::argumentToArray($args[1]) : array();
117        $options = isset($args[2]) ? $args[2] : array();
118
119        if (!isset($options['debug'])) {
120            $options['debug'] = $this->factory->isDebug();
121        }
122
123        if (!is_array($options)) {
124            throw new \RuntimeException('The third argument must be omitted, null or an array.');
125        }
126
127        // apply the prototype options
128        $options += $protoOptions;
129
130        if (!isset($options['name'])) {
131            $options['name'] = $this->factory->generateAssetName($inputs, $filters, $options);
132        }
133
134        return array($options['name'] => array($inputs, $filters, $options));
135    }
136
137    /**
138     * Returns an array of prototypical calls and options.
139     *
140     * @return array Prototypes and options
141     */
142    abstract protected function registerPrototypes();
143
144    /**
145     * Returns setup code for the reflection scriptlet.
146     *
147     * @return string Some PHP setup code
148     */
149    abstract protected function registerSetupCode();
150
151    protected static function tokenToString($token)
152    {
153        return is_array($token) ? $token[1] : $token;
154    }
155
156    protected static function argumentToArray($argument)
157    {
158        return is_array($argument) ? $argument : array_filter(array_map('trim', explode(',', $argument)));
159    }
160}
161