acbcb3b8986a73bdc86d73eb8b24fd15b322d386
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Reflection / ClassSchema.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Reflection;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Doctrine\Common\Annotations\AnnotationReader;
18 use phpDocumentor\Reflection\DocBlock\Tags\Param;
19 use phpDocumentor\Reflection\DocBlockFactory;
20 use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
21 use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
22 use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
23 use Symfony\Component\PropertyInfo\Type;
24 use TYPO3\CMS\Core\SingletonInterface;
25 use TYPO3\CMS\Core\Utility\ClassNamingUtility;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Core\Utility\StringUtility;
28 use TYPO3\CMS\Extbase\Annotation\IgnoreValidation;
29 use TYPO3\CMS\Extbase\Annotation\Inject;
30 use TYPO3\CMS\Extbase\Annotation\ORM\Cascade;
31 use TYPO3\CMS\Extbase\Annotation\ORM\Lazy;
32 use TYPO3\CMS\Extbase\Annotation\ORM\Transient;
33 use TYPO3\CMS\Extbase\Annotation\Validate;
34 use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
35 use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
36 use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface;
37 use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException;
38 use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
39 use TYPO3\CMS\Extbase\Reflection\ClassSchema\Method;
40 use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
41 use TYPO3\CMS\Extbase\Reflection\DocBlock\Tags\Null_;
42 use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
43 use TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException;
44 use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException;
45 use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
46
47 /**
48 * A class schema
49 * @internal only to be used within Extbase, not part of TYPO3 Core API.
50 */
51 class ClassSchema
52 {
53 /**
54 * Available model types
55 */
56 private const MODELTYPE_ENTITY = 1;
57 private const MODELTYPE_VALUEOBJECT = 2;
58
59 /**
60 * @var array
61 */
62 private static $propertyObjects = [];
63
64 /**
65 * @var array
66 */
67 private static $methodObjects = [];
68
69 /**
70 * Name of the class this schema is referring to
71 *
72 * @var string
73 */
74 protected $className;
75
76 /**
77 * Model type of the class this schema is referring to
78 *
79 * @var int
80 */
81 protected $modelType = self::MODELTYPE_ENTITY;
82
83 /**
84 * Whether a repository exists for the class this schema is referring to
85 *
86 * @var bool
87 */
88 protected $aggregateRoot = false;
89
90 /**
91 * Properties of the class which need to be persisted
92 *
93 * @var array
94 */
95 protected $properties = [];
96
97 /**
98 * Indicates if the class is a singleton or not.
99 *
100 * @var bool
101 */
102 private $isSingleton;
103
104 /**
105 * @var bool
106 */
107 private $isController;
108
109 /**
110 * @var array
111 */
112 private $methods = [];
113
114 /**
115 * @var array
116 */
117 private $injectProperties = [];
118
119 /**
120 * @var array
121 */
122 private $injectMethods = [];
123
124 /**
125 * @var PropertyInfoExtractor
126 */
127 private static $propertyInfoExtractor;
128
129 /**
130 * @var
131 */
132 private static $docBlockFactory;
133
134 /**
135 * Constructs this class schema
136 *
137 * @param string $className Name of the class this schema is referring to
138 * @throws \TYPO3\CMS\Extbase\Reflection\Exception\UnknownClassException
139 * @throws \ReflectionException
140 */
141 public function __construct($className)
142 {
143 $this->className = $className;
144
145 $reflectionClass = new \ReflectionClass($className);
146
147 $this->isSingleton = $reflectionClass->implementsInterface(SingletonInterface::class);
148 $this->isController = $reflectionClass->implementsInterface(ControllerInterface::class);
149
150 if ($reflectionClass->isSubclassOf(AbstractEntity::class)) {
151 $this->modelType = static::MODELTYPE_ENTITY;
152
153 $possibleRepositoryClassName = ClassNamingUtility::translateModelNameToRepositoryName($className);
154 if (class_exists($possibleRepositoryClassName)) {
155 $this->setAggregateRoot(true);
156 }
157 }
158
159 if ($reflectionClass->isSubclassOf(AbstractValueObject::class)) {
160 $this->modelType = static::MODELTYPE_VALUEOBJECT;
161 }
162
163 if (self::$propertyInfoExtractor === null) {
164 $docBlockFactory = DocBlockFactory::createInstance();
165 $docBlockFactory->registerTagHandler('var', DocBlock\Tags\Var_::class);
166
167 $phpDocExtractor = new PhpDocExtractor($docBlockFactory);
168 $reflectionExtractor = new ReflectionExtractor();
169
170 self::$propertyInfoExtractor = new PropertyInfoExtractor(
171 [],
172 [$phpDocExtractor, $reflectionExtractor]
173 );
174 }
175
176 if (self::$docBlockFactory === null) {
177 self::$docBlockFactory = DocBlockFactory::createInstance();
178 self::$docBlockFactory->registerTagHandler('author', Null_::class);
179 self::$docBlockFactory->registerTagHandler('covers', Null_::class);
180 self::$docBlockFactory->registerTagHandler('deprecated', Null_::class);
181 self::$docBlockFactory->registerTagHandler('link', Null_::class);
182 self::$docBlockFactory->registerTagHandler('method', Null_::class);
183 self::$docBlockFactory->registerTagHandler('property-read', Null_::class);
184 self::$docBlockFactory->registerTagHandler('property', Null_::class);
185 self::$docBlockFactory->registerTagHandler('property-write', Null_::class);
186 self::$docBlockFactory->registerTagHandler('return', Null_::class);
187 self::$docBlockFactory->registerTagHandler('see', Null_::class);
188 self::$docBlockFactory->registerTagHandler('since', Null_::class);
189 self::$docBlockFactory->registerTagHandler('source', Null_::class);
190 self::$docBlockFactory->registerTagHandler('throw', Null_::class);
191 self::$docBlockFactory->registerTagHandler('throws', Null_::class);
192 self::$docBlockFactory->registerTagHandler('uses', Null_::class);
193 self::$docBlockFactory->registerTagHandler('var', Null_::class);
194 self::$docBlockFactory->registerTagHandler('version', Null_::class);
195 }
196
197 $this->reflectProperties($reflectionClass);
198 $this->reflectMethods($reflectionClass);
199 }
200
201 /**
202 * @param \ReflectionClass $reflectionClass
203 */
204 protected function reflectProperties(\ReflectionClass $reflectionClass)
205 {
206 $annotationReader = new AnnotationReader();
207
208 $defaultProperties = $reflectionClass->getDefaultProperties();
209
210 foreach ($reflectionClass->getProperties() as $reflectionProperty) {
211 $propertyName = $reflectionProperty->getName();
212
213 $this->properties[$propertyName] = [
214 'default' => $reflectionProperty->isDefault(),
215 'defaultValue' => $defaultProperties[$propertyName] ?? null,
216 'private' => $reflectionProperty->isPrivate(),
217 'protected' => $reflectionProperty->isProtected(),
218 'public' => $reflectionProperty->isPublic(),
219 'static' => $reflectionProperty->isStatic(),
220 'type' => null, // Extbase
221 'elementType' => null, // Extbase
222 'annotations' => [],
223 'tags' => [],
224 'validators' => []
225 ];
226
227 $this->properties[$propertyName]['annotations']['inject'] = false;
228 $this->properties[$propertyName]['annotations']['lazy'] = false;
229 $this->properties[$propertyName]['annotations']['transient'] = false;
230 $this->properties[$propertyName]['annotations']['type'] = null;
231 $this->properties[$propertyName]['annotations']['cascade'] = null;
232 $this->properties[$propertyName]['annotations']['dependency'] = null;
233
234 $annotations = $annotationReader->getPropertyAnnotations($reflectionProperty);
235
236 /** @var array|Validate[] $validateAnnotations */
237 $validateAnnotations = array_filter($annotations, function ($annotation) {
238 return $annotation instanceof Validate;
239 });
240
241 if (count($validateAnnotations) > 0) {
242 $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class);
243
244 foreach ($validateAnnotations as $validateAnnotation) {
245 $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validateAnnotation->validator);
246
247 $this->properties[$propertyName]['validators'][] = [
248 'name' => $validateAnnotation->validator,
249 'options' => $validateAnnotation->options,
250 'className' => $validatorObjectName,
251 ];
252 }
253 }
254
255 if ($annotationReader->getPropertyAnnotation($reflectionProperty, Lazy::class) instanceof Lazy) {
256 $this->properties[$propertyName]['annotations']['lazy'] = true;
257 }
258
259 if ($annotationReader->getPropertyAnnotation($reflectionProperty, Transient::class) instanceof Transient) {
260 $this->properties[$propertyName]['annotations']['transient'] = true;
261 }
262
263 $isInjectProperty = $propertyName !== 'settings'
264 && ($annotationReader->getPropertyAnnotation($reflectionProperty, Inject::class) instanceof Inject);
265
266 /** @var Type[] $types */
267 $types = (array)self::$propertyInfoExtractor->getTypes($this->className, $propertyName);
268 $typesCount = count($types);
269
270 if ($typesCount > 0
271 && ($annotation = $annotationReader->getPropertyAnnotation($reflectionProperty, Cascade::class)) instanceof Cascade
272 ) {
273 /** @var Cascade $annotation */
274 $this->properties[$propertyName]['annotations']['cascade'] = $annotation->value;
275 }
276
277 if ($isInjectProperty && ($type = $types[0]) instanceof Type) {
278 $this->properties[$propertyName]['annotations']['inject'] = true;
279 $this->properties[$propertyName]['annotations']['type'] = $type->getClassName();
280 $this->properties[$propertyName]['annotations']['dependency'] = $type->getClassName();
281
282 $this->injectProperties[] = $propertyName;
283 }
284
285 if ($typesCount === 1) {
286 $this->properties[$propertyName]['type'] = $types[0]->getClassName() ?? $types[0]->getBuiltinType();
287 } elseif ($typesCount === 2) {
288 [$type, $elementType] = $types;
289 $actualType = $type->getClassName() ?? $type->getBuiltinType();
290
291 if (TypeHandlingUtility::isCollectionType($actualType)
292 && $elementType->getBuiltinType() === 'array'
293 && $elementType->getCollectionValueType() instanceof Type
294 && $elementType->getCollectionValueType()->getClassName() !== null
295 ) {
296 $this->properties[$propertyName]['type'] = ltrim($actualType, '\\');
297 $this->properties[$propertyName]['elementType'] = ltrim($elementType->getCollectionValueType()->getClassName(), '\\');
298 }
299 }
300 }
301 }
302
303 /**
304 * @param \ReflectionClass $reflectionClass
305 */
306 protected function reflectMethods(\ReflectionClass $reflectionClass)
307 {
308 $annotationReader = new AnnotationReader();
309
310 foreach ($reflectionClass->getMethods() as $reflectionMethod) {
311 $methodName = $reflectionMethod->getName();
312
313 $this->methods[$methodName] = [];
314 $this->methods[$methodName]['private'] = $reflectionMethod->isPrivate();
315 $this->methods[$methodName]['protected'] = $reflectionMethod->isProtected();
316 $this->methods[$methodName]['public'] = $reflectionMethod->isPublic();
317 $this->methods[$methodName]['static'] = $reflectionMethod->isStatic();
318 $this->methods[$methodName]['abstract'] = $reflectionMethod->isAbstract();
319 $this->methods[$methodName]['params'] = [];
320 $this->methods[$methodName]['tags'] = [];
321 $this->methods[$methodName]['annotations'] = [];
322 $this->methods[$methodName]['isAction'] = StringUtility::endsWith($methodName, 'Action');
323
324 $argumentValidators = [];
325
326 $annotations = $annotationReader->getMethodAnnotations($reflectionMethod);
327
328 /** @var array|Validate[] $validateAnnotations */
329 $validateAnnotations = array_filter($annotations, function ($annotation) {
330 return $annotation instanceof Validate;
331 });
332
333 if ($this->isController && $this->methods[$methodName]['isAction'] && count($validateAnnotations) > 0) {
334 $validatorResolver = GeneralUtility::makeInstance(ValidatorResolver::class);
335
336 foreach ($validateAnnotations as $validateAnnotation) {
337 $validatorName = $validateAnnotation->validator;
338 $validatorObjectName = $validatorResolver->resolveValidatorObjectName($validatorName);
339
340 $argumentValidators[$validateAnnotation->param][] = [
341 'name' => $validatorName,
342 'options' => $validateAnnotation->options,
343 'className' => $validatorObjectName,
344 ];
345 }
346 }
347
348 foreach ($annotations as $annotation) {
349 if ($annotation instanceof IgnoreValidation) {
350 $this->methods[$methodName]['tags']['ignorevalidation'][] = $annotation->argumentName;
351 }
352 }
353
354 $docComment = $reflectionMethod->getDocComment();
355 $docComment = is_string($docComment) ? $docComment : '';
356
357 foreach ($reflectionMethod->getParameters() as $parameterPosition => $reflectionParameter) {
358 /* @var \ReflectionParameter $reflectionParameter */
359
360 $parameterName = $reflectionParameter->getName();
361
362 $ignoreValidationParameters = array_filter($annotations, function ($annotation) use ($parameterName) {
363 return $annotation instanceof IgnoreValidation && $annotation->argumentName === $parameterName;
364 });
365
366 $this->methods[$methodName]['params'][$parameterName] = [];
367 $this->methods[$methodName]['params'][$parameterName]['position'] = $parameterPosition; // compat
368 $this->methods[$methodName]['params'][$parameterName]['byReference'] = $reflectionParameter->isPassedByReference(); // compat
369 $this->methods[$methodName]['params'][$parameterName]['array'] = $reflectionParameter->isArray(); // compat
370 $this->methods[$methodName]['params'][$parameterName]['optional'] = $reflectionParameter->isOptional();
371 $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionParameter->allowsNull();
372 $this->methods[$methodName]['params'][$parameterName]['class'] = null; // compat
373 $this->methods[$methodName]['params'][$parameterName]['type'] = null;
374 $this->methods[$methodName]['params'][$parameterName]['hasDefaultValue'] = $reflectionParameter->isDefaultValueAvailable();
375 $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = null;
376 $this->methods[$methodName]['params'][$parameterName]['dependency'] = null; // Extbase DI
377 $this->methods[$methodName]['params'][$parameterName]['ignoreValidation'] = count($ignoreValidationParameters) === 1;
378 $this->methods[$methodName]['params'][$parameterName]['validators'] = [];
379
380 if ($reflectionParameter->isDefaultValueAvailable()) {
381 $this->methods[$methodName]['params'][$parameterName]['defaultValue'] = $reflectionParameter->getDefaultValue();
382 }
383
384 if (($reflectionType = $reflectionParameter->getType()) instanceof \ReflectionType) {
385 $this->methods[$methodName]['params'][$parameterName]['type'] = (string)$reflectionType;
386 $this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionType->allowsNull();
387 }
388
389 if (($parameterClass = $reflectionParameter->getClass()) instanceof \ReflectionClass) {
390 $this->methods[$methodName]['params'][$parameterName]['class'] = $parameterClass->getName();
391 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($parameterClass->getName(), '\\');
392 }
393
394 if ($docComment !== '' && $this->methods[$methodName]['params'][$parameterName]['type'] === null) {
395 /*
396 * We create (redundant) instances here in this loop due to the fact that
397 * we do not want to analyse all doc blocks of all available methods. We
398 * use this technique only if we couldn't grasp all necessary data via
399 * reflection.
400 *
401 * Also, if we analyze all method doc blocks, we will trigger numerous errors
402 * due to non PSR-5 compatible tags in the core and in user land code.
403 *
404 * Fetching the data type via doc blocks will also be deprecated and removed
405 * in the near future.
406 */
407 $params = self::$docBlockFactory->create($docComment)
408 ->getTagsByName('param');
409
410 if (isset($params[$parameterPosition])) {
411 /** @var Param $param */
412 $param = $params[$parameterPosition];
413 $this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($param->getType(), '\\');
414 }
415 }
416
417 // Extbase DI
418 if ($reflectionParameter->getClass() instanceof \ReflectionClass
419 && ($reflectionMethod->isConstructor() || $this->hasInjectMethodName($reflectionMethod))
420 ) {
421 $this->methods[$methodName]['params'][$parameterName]['dependency'] = $reflectionParameter->getClass()->getName();
422 }
423
424 // Extbase Validation
425 if (isset($argumentValidators[$parameterName])) {
426 if ($this->methods[$methodName]['params'][$parameterName]['type'] === null) {
427 throw new InvalidTypeHintException(
428 'Missing type information for parameter "$' . $parameterName . '" in ' . $this->className . '->' . $methodName . '(): Either use an @param annotation or use a type hint.',
429 1515075192
430 );
431 }
432
433 $this->methods[$methodName]['params'][$parameterName]['validators'] = $argumentValidators[$parameterName];
434 unset($argumentValidators[$parameterName]);
435 }
436 }
437
438 // Extbase Validation
439 foreach ($argumentValidators as $parameterName => $validators) {
440 $validatorNames = array_column($validators, 'name');
441
442 throw new InvalidValidationConfigurationException(
443 'Invalid validate annotation in ' . $this->className . '->' . $methodName . '(): The following validators have been defined for missing param "$' . $parameterName . '": ' . implode(', ', $validatorNames),
444 1515073585
445 );
446 }
447
448 // Extbase
449 $this->methods[$methodName]['injectMethod'] = false;
450 if ($this->hasInjectMethodName($reflectionMethod)
451 && count($this->methods[$methodName]['params']) === 1
452 && reset($this->methods[$methodName]['params'])['dependency'] !== null
453 ) {
454 $this->methods[$methodName]['injectMethod'] = true;
455 $this->injectMethods[] = $methodName;
456 }
457 }
458 }
459
460 /**
461 * Returns the class name this schema is referring to
462 *
463 * @return string The class name
464 */
465 public function getClassName(): string
466 {
467 return $this->className;
468 }
469
470 /**
471 * @throws NoSuchPropertyException
472 *
473 * @param string $propertyName
474 * @return Property
475 */
476 public function getProperty(string $propertyName): Property
477 {
478 $properties = $this->buildPropertyObjects();
479
480 if (!isset($properties[$propertyName])) {
481 throw NoSuchPropertyException::create($this->className, $propertyName);
482 }
483
484 return $properties[$propertyName];
485 }
486
487 /**
488 * @return array|Property[]
489 */
490 public function getProperties(): array
491 {
492 return $this->buildPropertyObjects();
493 }
494
495 /**
496 * Marks the class if it is root of an aggregate and therefore accessible
497 * through a repository - or not.
498 *
499 * @param bool $isRoot TRUE if it is the root of an aggregate
500 */
501 public function setAggregateRoot($isRoot)
502 {
503 $this->aggregateRoot = $isRoot;
504 }
505
506 /**
507 * Whether the class is an aggregate root and therefore accessible through
508 * a repository.
509 *
510 * @return bool TRUE if it is managed
511 */
512 public function isAggregateRoot(): bool
513 {
514 return $this->aggregateRoot;
515 }
516
517 /**
518 * If the class schema has a certain property.
519 *
520 * @param string $propertyName Name of the property
521 * @return bool
522 */
523 public function hasProperty($propertyName): bool
524 {
525 return array_key_exists($propertyName, $this->properties);
526 }
527
528 /**
529 * @return bool
530 */
531 public function hasConstructor(): bool
532 {
533 return isset($this->methods['__construct']);
534 }
535
536 /**
537 * @throws NoSuchMethodException
538 *
539 * @param string $methodName
540 * @return Method
541 */
542 public function getMethod(string $methodName): Method
543 {
544 $methods = $this->buildMethodObjects();
545
546 if (!isset($methods[$methodName])) {
547 throw NoSuchMethodException::create($this->className, $methodName);
548 }
549
550 return $methods[$methodName];
551 }
552
553 /**
554 * @return array|Method[]
555 */
556 public function getMethods(): array
557 {
558 return $this->buildMethodObjects();
559 }
560
561 /**
562 * @param \ReflectionMethod $reflectionMethod
563 * @return bool
564 */
565 protected function hasInjectMethodName(\ReflectionMethod $reflectionMethod): bool
566 {
567 $methodName = $reflectionMethod->getName();
568 if ($methodName === 'injectSettings' || !$reflectionMethod->isPublic()) {
569 return false;
570 }
571
572 if (
573 strpos($reflectionMethod->getName(), 'inject') === 0
574 ) {
575 return true;
576 }
577
578 return false;
579 }
580
581 /**
582 * @return bool
583 * @internal
584 */
585 public function isModel(): bool
586 {
587 return $this->isEntity() || $this->isValueObject();
588 }
589
590 /**
591 * @return bool
592 * @internal
593 */
594 public function isEntity(): bool
595 {
596 return $this->modelType === static::MODELTYPE_ENTITY;
597 }
598
599 /**
600 * @return bool
601 * @internal
602 */
603 public function isValueObject(): bool
604 {
605 return $this->modelType === static::MODELTYPE_VALUEOBJECT;
606 }
607
608 /**
609 * @return bool
610 */
611 public function isSingleton(): bool
612 {
613 return $this->isSingleton;
614 }
615
616 /**
617 * @param string $methodName
618 * @return bool
619 */
620 public function hasMethod(string $methodName): bool
621 {
622 return isset($this->methods[$methodName]);
623 }
624
625 /**
626 * @return bool
627 */
628 public function hasInjectProperties(): bool
629 {
630 return count($this->injectProperties) > 0;
631 }
632
633 /**
634 * @return bool
635 */
636 public function hasInjectMethods(): bool
637 {
638 return count($this->injectMethods) > 0;
639 }
640
641 /**
642 * @return array|Method[]
643 */
644 public function getInjectMethods(): array
645 {
646 return array_filter($this->buildMethodObjects(), function ($method) {
647 /** @var Method $method */
648 return $method->isInjectMethod();
649 });
650 }
651
652 /**
653 * @return array
654 */
655 public function getInjectProperties(): array
656 {
657 $injectProperties = [];
658 foreach ($this->injectProperties as $injectPropertyName) {
659 $injectProperties[$injectPropertyName] = $this->properties[$injectPropertyName]['annotations']['dependency'];
660 }
661
662 return $injectProperties;
663 }
664
665 /**
666 * @return array
667 */
668 private function buildPropertyObjects(): array
669 {
670 if (!isset(static::$propertyObjects[$this->className])) {
671 static::$propertyObjects[$this->className] = [];
672 foreach ($this->properties as $propertyName => $propertyDefinition) {
673 static::$propertyObjects[$this->className][$propertyName] = new Property($propertyName, $propertyDefinition);
674 }
675 }
676
677 return static::$propertyObjects[$this->className];
678 }
679
680 /**
681 * @return array|Method[]
682 */
683 private function buildMethodObjects(): array
684 {
685 if (!isset(static::$methodObjects[$this->className])) {
686 static::$methodObjects[$this->className] = [];
687 foreach ($this->methods as $methodName => $methodDefinition) {
688 static::$methodObjects[$this->className][$methodName] = new Method($methodName, $methodDefinition, $this->className);
689 }
690 }
691
692 return static::$methodObjects[$this->className];
693 }
694 }