Completed
Push — master ( be7e90...e1942b )
by Anton
03:20
created

Supervisor::createNode()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 6
nop 2
dl 0
loc 21
rs 9.0534
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Stempler;
10
11
use Spiral\Stempler\Behaviours\ExtendLayout;
12
use Spiral\Stempler\Behaviours\IncludeBlock;
13
use Spiral\Stempler\Behaviours\InnerBlock;
14
use Spiral\Stempler\Exceptions\LoaderExceptionInterface;
15
use Spiral\Stempler\Exceptions\StemplerException;
16
use Spiral\Stempler\Importers\Stopper;
17
18
/**
19
 * Supervisors used to control node behaviours and syntax.
20
 */
21
class Supervisor implements SupervisorInterface
22
{
23
    /**
24
     * Used to create unique node names when required.
25
     *
26
     * @var int
27
     */
28
    private static $index = 0;
29
30
    /**
31
     * Active set of imports.
32
     *
33
     * @var ImporterInterface[]
34
     */
35
    private $importers = [];
36
37
    /**
38
     * @var SyntaxInterface
39
     */
40
    protected $syntax = null;
41
42
    /**
43
     * @var LoaderInterface
44
     */
45
    protected $loader = null;
46
47
    /**
48
     * @param LoaderInterface $loader
49
     * @param SyntaxInterface $syntax
50
     */
51
    public function __construct(LoaderInterface $loader, SyntaxInterface $syntax)
52
    {
53
        $this->loader = $loader;
54
        $this->syntax = $syntax;
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function getSyntax(): SyntaxInterface
61
    {
62
        return $this->syntax;
63
    }
64
65
    /**
66
     * Add new elements import locator.
67
     *
68
     * @param ImporterInterface $import
69
     */
70
    public function registerImporter(ImporterInterface $import)
71
    {
72
        array_unshift($this->importers, $import);
73
    }
74
75
    /**
76
     * Active templater imports.
77
     *
78
     * @return ImporterInterface[]
79
     */
80
    public function getImporters()
81
    {
82
        return $this->importers;
83
    }
84
85
    /**
86
     * Remove all element importers.
87
     */
88
    public function flushImporters()
89
    {
90
        $this->importers = [];
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96
    public function tokenBehaviour(array $token, array $content, Node $node)
97
    {
98
        switch ($this->syntax->tokenType($token, $name)) {
99
            case SyntaxInterface::TYPE_BLOCK:
100
                //Tag declares block (section)
101
                return new InnerBlock($name);
102
103
            case SyntaxInterface::TYPE_EXTENDS:
104
                //Declares parent extending
105
                $extends = new ExtendLayout(
106
                    $this->createNode($this->syntax->resolvePath($token), $token),
107
                    $token
108
                );
109
110
                //We have to combine parent imports with local one (this is why uses has to be defined
111
                //after extends tag!)
112
                $this->importers = $extends->parentImports();
113
114
                //Sending command to extend parent
115
                return $extends;
116
117
            case SyntaxInterface::TYPE_IMPORTER:
118
                //Implementation specific
119
                $this->registerImporter($this->syntax->createImporter($token, $this));
120
121
                //No need to include import tag into source
122
                return BehaviourInterface::SKIP_TOKEN;
123
        }
124
125
        //We now have to decide if element points to external view (source) to be imported
126
        foreach ($this->importers as $importer) {
127
            if ($importer->importable($name, $token)) {
128
                if ($importer instanceof Stopper) {
129
                    //Native importer tells us to treat this element as simple html
130
                    break;
131
                }
132
133
                //Let's include!
134
                return new IncludeBlock(
135
                    $this, $importer->resolvePath($name, $token), $content, $token
136
                );
137
            }
138
        }
139
140
        return BehaviourInterface::SIMPLE_TAG;
141
    }
142
143
    /**
144
     * Create node based on given location with identical supervisor (cloned).
145
     *
146
     * @param string $path
147
     * @param array  $token Context token.
148
     *
149
     * @return Node
150
     * @throws StemplerException
151
     */
152
    public function createNode(string $path, array $token = []): Node
153
    {
154
        //We support dots!
155
        if (!empty($token)) {
156
            $path = str_replace('.', '/', $path);
157
        }
158
159
        try {
160
            $context = $this->loader->getSourceContext($path);
161
        } catch (LoaderExceptionInterface $e) {
162
            throw new StemplerException($e->getMessage(), $token, 0, $e);
163
        }
164
165
        try {
166
            //In isolation
167
            return new Node(clone $this, $this->uniquePlaceholder(), $context->getSource());
168
        } catch (StemplerException $e) {
169
            //Wrapping to clarify location of error
170
            throw $this->clarifyException($context, $e);
171
        }
172
    }
173
174
    /**
175
     * Get unique placeholder name, unique names are required in some cases to correctly process
176
     * includes and etc.
177
     *
178
     * @return string
179
     */
180
    public function uniquePlaceholder(): string
181
    {
182
        return md5(self::$index++);
183
    }
184
185
    /**
186
     * Clarify exeption with it's actual location.
187
     *
188
     * @param StemplerSource    $sourceContext
189
     * @param StemplerException $exception
190
     *
191
     * @return StemplerException
192
     */
193
    protected function clarifyException(
194
        StemplerSource $sourceContext,
195
        StemplerException $exception
196
    ) {
197
        if (empty($exception->getToken())) {
198
            //Unable to locate
199
            return $exception;
200
        }
201
202
        //We will need only first tag line
203
        $target = explode("\n", $exception->getToken()[HtmlTokenizer::TOKEN_CONTENT])[0];
204
205
        //Let's try to locate place where exception was used
206
        $lines = explode("\n", $sourceContext->getSource());
207
208
        foreach ($lines as $number => $line) {
209
            if (strpos($line, $target) !== false) {
210
                //We found where token were used (!!)
211
                $exception->setLocation(
212
                    $sourceContext->getFilename(),
213
                    $number + 1
214
                );
215
216
                break;
217
            }
218
        }
219
220
        return $exception;
221
    }
222
}