15
15
use phpDocumentor \Reflection \DocBlock \Tags \Generic ;
16
16
use phpDocumentor \Reflection \FqsenResolver ;
17
17
use phpDocumentor \Reflection \Types \Context ;
18
+ use Webmozart \Assert \Assert ;
18
19
20
+ /**
21
+ * Creates a Tag object given the contents of a tag.
22
+ *
23
+ * This Factory is capable of determining the appropriate class for a tag and instantiate it using its `create`
24
+ * factory method. The `create` factory method of a Tag can have a variable number of arguments; this way you can
25
+ * pass the dependencies that you need to construct a tag object.
26
+ *
27
+ * > Important: each parameter in addition to the body variable for the `create` method must default to null, otherwise
28
+ * > it violates the constraint with the interface; it is recommended to use the {@see Assert::notNull()} method to
29
+ * > verify that a dependency is actually passed.
30
+ *
31
+ * This Factory also features a Service Locator component that is used to pass the right dependencies to the
32
+ * `create` method of a tag; each dependency should be registered as a service or as a parameter.
33
+ *
34
+ * When you want to use a Tag of your own with custom handling you need to call the `registerTagHandler` method, pass
35
+ * the name of the tag and a Fully Qualified Class Name pointing to a class that implements the Tag interface.
36
+ */
19
37
final class StandardTagFactory implements TagFactory
20
38
{
21
39
/** PCRE regular expression matching a tag name. */
22
40
const REGEX_TAGNAME = '[\w\-\_ \\\\]+ ' ;
23
41
24
42
/**
25
- * @var array An array with a tag as a key, and an FQCN to a class that handles it as an array value.
43
+ * @var string[] An array with a tag as a key, and an FQCN to a class that handles it as an array value.
26
44
*/
27
- private $ tagHandlerMappings = array (
45
+ private $ tagHandlerMappings = [
28
46
'author ' => '\phpDocumentor\Reflection\DocBlock\Tags\Author ' ,
29
47
'covers ' => '\phpDocumentor\Reflection\DocBlock\Tags\Covers ' ,
30
48
'deprecated ' => '\phpDocumentor\Reflection\DocBlock\Tags\Deprecated ' ,
31
- 'example ' => '\phpDocumentor\Reflection\DocBlock\Tags\Example ' ,
49
+ // 'example' => '\phpDocumentor\Reflection\DocBlock\Tags\Example',
32
50
'link ' => '\phpDocumentor\Reflection\DocBlock\Tags\Link ' ,
33
51
'method ' => '\phpDocumentor\Reflection\DocBlock\Tags\Method ' ,
34
52
'param ' => '\phpDocumentor\Reflection\DocBlock\Tags\Param ' ,
@@ -44,141 +62,246 @@ final class StandardTagFactory implements TagFactory
44
62
'uses ' => '\phpDocumentor\Reflection\DocBlock\Tags\Uses ' ,
45
63
'var ' => '\phpDocumentor\Reflection\DocBlock\Tags\Var_ ' ,
46
64
'version ' => '\phpDocumentor\Reflection\DocBlock\Tags\Version '
47
- );
65
+ ];
66
+
67
+ /**
68
+ * @var \ReflectionParameter[][] a lazy-loading cache containing parameters for each tagHandler that has been used.
69
+ */
70
+ private $ tagHandlerParameterCache = [];
48
71
49
- /** @var FqsenResolver */
72
+ /**
73
+ * @var FqsenResolver
74
+ */
50
75
private $ fqsenResolver ;
51
76
52
- /** @var mixed[] */
77
+ /**
78
+ * @var mixed[] an array representing a simple Service Locator where we can store parameters and
79
+ * services that can be inserted into the Factory Methods of Tag Handlers.
80
+ */
53
81
private $ serviceLocator = [];
54
82
55
- public function __construct (FqsenResolver $ fqsenResolver )
83
+ /**
84
+ * Initialize this tag factory with the means to resolve an FQSEN and optionally a list of tag handlers.
85
+ *
86
+ * If no tag handlers are provided than the default list in the {@see self::$tagHandlerMappings} property
87
+ * is used.
88
+ *
89
+ * @param FqsenResolver $fqsenResolver
90
+ * @param string[] $tagHandlers
91
+ *
92
+ * @see self::registerTagHandler() to add a new tag handler to the existing default list.
93
+ */
94
+ public function __construct (FqsenResolver $ fqsenResolver , array $ tagHandlers = null )
56
95
{
57
96
$ this ->fqsenResolver = $ fqsenResolver ;
58
- $ this ->addService ($ fqsenResolver );
97
+ if ($ tagHandlers !== null ) {
98
+ $ this ->tagHandlerMappings = $ tagHandlers ;
99
+ }
100
+
101
+ $ this ->addService ($ fqsenResolver , FqsenResolver::class);
102
+ }
103
+
104
+ /**
105
+ * {@inheritDoc}
106
+ */
107
+ public function create ($ tagLine , Context $ context = null )
108
+ {
109
+ if (! $ context ) {
110
+ $ context = new Context ('' );
111
+ }
112
+
113
+ list ($ tagName , $ tagBody ) = $ this ->extractTagParts ($ tagLine );
114
+
115
+ return $ this ->createTag ($ tagBody , $ tagName , $ context );
59
116
}
60
117
118
+ /**
119
+ * {@inheritDoc}
120
+ */
61
121
public function addParameter ($ name , $ value )
62
122
{
63
123
$ this ->serviceLocator [$ name ] = $ value ;
64
124
}
65
125
66
- public function addService ($ service )
126
+ /**
127
+ * {@inheritDoc}
128
+ */
129
+ public function addService ($ service , $ alias = null )
67
130
{
68
- $ this ->serviceLocator [get_class ($ service )] = $ service ;
131
+ $ this ->serviceLocator [$ alias ?: get_class ($ service )] = $ service ;
69
132
}
70
133
71
134
/**
72
- * Factory method responsible for instantiating the correct sub type.
73
- *
74
- * @param string $tagLine The text for this tag, including description.
75
- * @param Context $context
135
+ * {@inheritDoc}
136
+ */
137
+ public function registerTagHandler ($ tagName , $ handler )
138
+ {
139
+ Assert::stringNotEmpty ($ tagName );
140
+ Assert::stringNotEmpty ($ handler );
141
+ Assert::classExists ($ handler );
142
+ Assert::implementsInterface ($ handler , Tag::class);
143
+
144
+ if (strpos ($ tagName , '\\' ) && $ tagName [0 ] !== '\\' ) {
145
+ throw new \InvalidArgumentException (
146
+ 'A namespaced tag must have a leading backslash as it must be fully qualified '
147
+ );
148
+ }
149
+
150
+ $ this ->tagHandlerMappings [$ tagName ] = $ handler ;
151
+ }
152
+
153
+ /**
154
+ * Extracts all components for a tag.
76
155
*
77
- * @throws \InvalidArgumentException if an invalid tag line was presented.
156
+ * @param string $tagLine
78
157
*
79
- * @return static A new tag object.
158
+ * @return string[]
80
159
*/
81
- public function create ($ tagLine, Context $ context = null )
160
+ private function extractTagParts ($ tagLine )
82
161
{
83
- if (! $ context ) {
84
- $ context = new Context ('' );
162
+ $ matches = array ();
163
+ if (! preg_match ('/^@( ' . self ::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)?/us ' , $ tagLine , $ matches )) {
164
+ throw new \InvalidArgumentException (
165
+ 'The tag " ' . $ tagLine . '" does not seem to be wellformed, please check it for errors '
166
+ );
167
+ }
168
+
169
+ if (count ($ matches ) < 3 ) {
170
+ $ matches [] = '' ;
85
171
}
86
- list ($ tagName , $ tagBody ) = $ this ->extractTagParts ($ tagLine );
87
172
88
- $ handler = Generic::class;
173
+ return array_slice ($ matches , 1 );
174
+ }
175
+
176
+ /**
177
+ * Creates a new tag object with the given name and body or returns null if the tag name was recognized but the
178
+ * body was invalid.
179
+ *
180
+ * @param string $body
181
+ * @param string $name
182
+ * @param Context $context
183
+ *
184
+ * @return Tag|null
185
+ */
186
+ private function createTag ($ body , $ name , Context $ context )
187
+ {
188
+ $ handlerClassName = $ this ->findHandlerClassName ($ name , $ context );
189
+ $ arguments = $ this ->getArgumentsForParametersFromWiring (
190
+ $ this ->fetchParametersForHandlerFactoryMethod ($ handlerClassName ),
191
+ $ this ->getServiceLocatorWithDynamicParameters ($ context , $ name , $ body )
192
+ )
193
+ ;
194
+
195
+ return call_user_func_array ([$ handlerClassName , 'create ' ], $ arguments );
196
+ }
197
+
198
+ /**
199
+ * Determines the Fully Qualified Class Name of the Factory or Tag (containing a Factory Method `create`).
200
+ *
201
+ * @param string $tagName
202
+ * @param Context $context
203
+ *
204
+ * @return string
205
+ */
206
+ private function findHandlerClassName ($ tagName , Context $ context )
207
+ {
208
+ $ handlerClassName = Generic::class;
89
209
if (isset ($ this ->tagHandlerMappings [$ tagName ])) {
90
- $ handler = $ this ->tagHandlerMappings [$ tagName ];
210
+ $ handlerClassName = $ this ->tagHandlerMappings [$ tagName ];
91
211
} elseif ($ this ->isAnnotation ($ tagName )) {
92
- $ tagName = (string )$ this ->fqsenResolver ->resolve ($ tagName , $ context );
93
- if (isset ($ this ->tagHandlerMappings [$ tagName ])) {
94
- $ handler = $ this ->tagHandlerMappings [$ tagName ];
95
- }
212
+ // TODO: Annotation support is planned for a later stage and as such is disabled for now
213
+ // $tagName = (string)$this->fqsenResolver->resolve($tagName, $context);
214
+ // if (isset($this->annotationMappings[$tagName])) {
215
+ // $handlerClassName = $this->annotationMappings[$tagName];
216
+ // }
96
217
}
97
218
98
- $ parameters = (new \ReflectionMethod ($ handler , 'create ' ))->getParameters ();
99
-
100
- $ wiring = array_merge (
101
- $ this ->serviceLocator ,
102
- [
103
- 'name ' => $ tagName ,
104
- 'body ' => $ tagBody ,
105
- Context::class => $ context
106
- ]
107
- );
219
+ return $ handlerClassName ;
220
+ }
108
221
222
+ /**
223
+ * Retrieves the arguments that need to be passed to the Factory Method with the given Parameters.
224
+ *
225
+ * @param \ReflectionParameter[] $parameters
226
+ * @param mixed[] $locator
227
+ *
228
+ * @return mixed[] A series of values that can be passed to the Factory Method of the tag whose parameters
229
+ * is provided with this method.
230
+ */
231
+ private function getArgumentsForParametersFromWiring ($ parameters , $ locator )
232
+ {
109
233
$ arguments = [];
110
234
foreach ($ parameters as $ index => $ parameter ) {
111
235
$ typeHint = $ parameter ->getClass () ? $ parameter ->getClass ()->getName () : null ;
112
- if (isset ($ wiring [$ typeHint ])) {
113
- $ arguments [] = $ wiring [$ typeHint ];
236
+ if (isset ($ locator [$ typeHint ])) {
237
+ $ arguments [] = $ locator [$ typeHint ];
114
238
continue ;
115
239
}
116
240
117
241
$ parameterName = $ parameter ->getName ();
118
- if (isset ($ wiring [$ parameterName ])) {
119
- $ arguments [] = $ wiring [$ parameterName ];
242
+ if (isset ($ locator [$ parameterName ])) {
243
+ $ arguments [] = $ locator [$ parameterName ];
120
244
continue ;
121
245
}
122
246
123
247
$ arguments [] = null ;
124
248
}
125
249
126
- return call_user_func_array ([ $ handler , ' create ' ], $ arguments) ;
250
+ return $ arguments ;
127
251
}
128
252
129
253
/**
130
- * Registers a handler for tags.
131
- *
132
- * Registers a handler for tags. The class specified is autoloaded if it's not available. It must inherit from
133
- * this class.
254
+ * Retrieves a series of ReflectionParameter objects for the static 'create' method of the given
255
+ * tag handler class name.
134
256
*
135
- * @param string $tag Name of tag to register a handler for. When registering a namespaced tag, the full
136
- * name, along with a prefixing slash MUST be provided.
137
- * @param string|null $handler FQCN of handler.
257
+ * @param string $handlerClassName
138
258
*
139
- * @return bool TRUE on success, FALSE on failure.
259
+ * @return \ReflectionParameter[]
140
260
*/
141
- public function registerTagHandler ( $ tag , $ handler )
261
+ private function fetchParametersForHandlerFactoryMethod ( $ handlerClassName )
142
262
{
143
- $ tag = trim ((string )$ tag );
144
-
145
- if ('' !== $ tag
146
- && class_exists ($ handler )
147
- && is_subclass_of ($ handler , Tag::class)
148
- && ! strpos ($ tag , '\\' ) //Accept no slash, and 1st slash at offset 0.
149
- ) {
150
- $ this ->tagHandlerMappings [$ tag ] = $ handler ;
151
-
152
- return true ;
263
+ if (! isset ($ this ->tagHandlerParameterCache [$ handlerClassName ])) {
264
+ $ methodReflection = new \ReflectionMethod ($ handlerClassName , 'create ' );
265
+ $ this ->tagHandlerParameterCache [$ handlerClassName ] = $ methodReflection ->getParameters ();
153
266
}
154
267
155
- return false ;
268
+ return $ this -> tagHandlerParameterCache [ $ handlerClassName ] ;
156
269
}
157
270
158
271
/**
159
- * Extracts all components for a tag.
272
+ * Returns a copy of this class' Service Locator with added dynamic parameters, such as the tag's name, body and
273
+ * Context.
160
274
*
161
- * @param string $tagLine
275
+ * @param Context $context The Context (namespace and aliasses) that may be passed and is used to resolve FQSENs.
276
+ * @param string $tagName The name of the tag that may be passed onto the factory method of the Tag class.
277
+ * @param string $tagBody The body of the tag that may be passed onto the factory method of the Tag class.
162
278
*
163
- * @return string []
279
+ * @return mixed []
164
280
*/
165
- private function extractTagParts ( $ tagLine )
281
+ private function getServiceLocatorWithDynamicParameters ( Context $ context , $ tagName , $ tagBody )
166
282
{
167
- $ matches = array ();
168
- if (! preg_match ('/^@( ' . self ::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)?/us ' , $ tagLine , $ matches )) {
169
- throw new \InvalidArgumentException (
170
- 'The tag " ' . $ tagLine . '" does not seem to be wellformed, please check it for errors '
171
- );
172
- }
173
-
174
- if (count ($ matches ) < 3 ) {
175
- $ matches [] = '' ;
176
- }
283
+ $ locator = array_merge (
284
+ $ this ->serviceLocator ,
285
+ [
286
+ 'name ' => $ tagName ,
287
+ 'body ' => $ tagBody ,
288
+ Context::class => $ context
289
+ ]
290
+ );
177
291
178
- return array_slice ( $ matches , 1 ) ;
292
+ return $ locator ;
179
293
}
180
294
181
- private function isAnnotation ($ tag )
295
+ /**
296
+ * Returns whether the given tag belongs to an annotation.
297
+ *
298
+ * @param string $tagContent
299
+ *
300
+ * @todo this method should be populated once we implement Annotation notation support.
301
+ *
302
+ * @return bool
303
+ */
304
+ private function isAnnotation ($ tagContent )
182
305
{
183
306
// 1. Contains a namespace separator
184
307
// 2. Contains parenthesis
0 commit comments