1515use phpDocumentor \Reflection \DocBlock \Tags \Generic ;
1616use phpDocumentor \Reflection \FqsenResolver ;
1717use phpDocumentor \Reflection \Types \Context ;
18+ use Webmozart \Assert \Assert ;
1819
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+ */
1937final class StandardTagFactory implements TagFactory
2038{
2139 /** PCRE regular expression matching a tag name. */
2240 const REGEX_TAGNAME = '[\w\-\_ \\\\]+ ' ;
2341
2442 /**
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.
2644 */
27- private $ tagHandlerMappings = array (
45+ private $ tagHandlerMappings = [
2846 'author ' => '\phpDocumentor\Reflection\DocBlock\Tags\Author ' ,
2947 'covers ' => '\phpDocumentor\Reflection\DocBlock\Tags\Covers ' ,
3048 'deprecated ' => '\phpDocumentor\Reflection\DocBlock\Tags\Deprecated ' ,
31- 'example ' => '\phpDocumentor\Reflection\DocBlock\Tags\Example ' ,
49+ // 'example' => '\phpDocumentor\Reflection\DocBlock\Tags\Example',
3250 'link ' => '\phpDocumentor\Reflection\DocBlock\Tags\Link ' ,
3351 'method ' => '\phpDocumentor\Reflection\DocBlock\Tags\Method ' ,
3452 'param ' => '\phpDocumentor\Reflection\DocBlock\Tags\Param ' ,
@@ -44,141 +62,246 @@ final class StandardTagFactory implements TagFactory
4462 'uses ' => '\phpDocumentor\Reflection\DocBlock\Tags\Uses ' ,
4563 'var ' => '\phpDocumentor\Reflection\DocBlock\Tags\Var_ ' ,
4664 '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 = [];
4871
49- /** @var FqsenResolver */
72+ /**
73+ * @var FqsenResolver
74+ */
5075 private $ fqsenResolver ;
5176
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+ */
5381 private $ serviceLocator = [];
5482
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 )
5695 {
5796 $ 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 );
59116 }
60117
118+ /**
119+ * {@inheritDoc}
120+ */
61121 public function addParameter ($ name , $ value )
62122 {
63123 $ this ->serviceLocator [$ name ] = $ value ;
64124 }
65125
66- public function addService ($ service )
126+ /**
127+ * {@inheritDoc}
128+ */
129+ public function addService ($ service , $ alias = null )
67130 {
68- $ this ->serviceLocator [get_class ($ service )] = $ service ;
131+ $ this ->serviceLocator [$ alias ?: get_class ($ service )] = $ service ;
69132 }
70133
71134 /**
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.
76155 *
77- * @throws \InvalidArgumentException if an invalid tag line was presented.
156+ * @param string $tagLine
78157 *
79- * @return static A new tag object.
158+ * @return string[]
80159 */
81- public function create ($ tagLine, Context $ context = null )
160+ private function extractTagParts ($ tagLine )
82161 {
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 [] = '' ;
85171 }
86- list ($ tagName , $ tagBody ) = $ this ->extractTagParts ($ tagLine );
87172
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;
89209 if (isset ($ this ->tagHandlerMappings [$ tagName ])) {
90- $ handler = $ this ->tagHandlerMappings [$ tagName ];
210+ $ handlerClassName = $ this ->tagHandlerMappings [$ tagName ];
91211 } 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+ // }
96217 }
97218
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+ }
108221
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+ {
109233 $ arguments = [];
110234 foreach ($ parameters as $ index => $ parameter ) {
111235 $ 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 ];
114238 continue ;
115239 }
116240
117241 $ parameterName = $ parameter ->getName ();
118- if (isset ($ wiring [$ parameterName ])) {
119- $ arguments [] = $ wiring [$ parameterName ];
242+ if (isset ($ locator [$ parameterName ])) {
243+ $ arguments [] = $ locator [$ parameterName ];
120244 continue ;
121245 }
122246
123247 $ arguments [] = null ;
124248 }
125249
126- return call_user_func_array ([ $ handler , ' create ' ], $ arguments) ;
250+ return $ arguments ;
127251 }
128252
129253 /**
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.
134256 *
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
138258 *
139- * @return bool TRUE on success, FALSE on failure.
259+ * @return \ReflectionParameter[]
140260 */
141- public function registerTagHandler ( $ tag , $ handler )
261+ private function fetchParametersForHandlerFactoryMethod ( $ handlerClassName )
142262 {
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 ();
153266 }
154267
155- return false ;
268+ return $ this -> tagHandlerParameterCache [ $ handlerClassName ] ;
156269 }
157270
158271 /**
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.
160274 *
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.
162278 *
163- * @return string []
279+ * @return mixed []
164280 */
165- private function extractTagParts ( $ tagLine )
281+ private function getServiceLocatorWithDynamicParameters ( Context $ context , $ tagName , $ tagBody )
166282 {
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+ );
177291
178- return array_slice ( $ matches , 1 ) ;
292+ return $ locator ;
179293 }
180294
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 )
182305 {
183306 // 1. Contains a namespace separator
184307 // 2. Contains parenthesis
0 commit comments