setExtends() public method

public setExtends ( $types ) : self
return self
示例#1
0
文件: DIExtension.php 项目: nette/di
 public function afterCompile(Nette\PhpGenerator\ClassType $class)
 {
     if ($this->config['parentClass']) {
         $class->setExtends($this->config['parentClass']);
     }
     $initialize = $class->getMethod('initialize');
     $builder = $this->getContainerBuilder();
     if ($this->debugMode && $this->config['debugger']) {
         Nette\Bridges\DITracy\ContainerPanel::$compilationTime = $this->time;
         $initialize->addBody($builder->formatPhp('?;', [new Nette\DI\Statement('@Tracy\\Bar::addPanel', [new Nette\DI\Statement(Nette\Bridges\DITracy\ContainerPanel::class)])]));
     }
     foreach (array_filter($builder->findByTag('run')) as $name => $on) {
         $initialize->addBody('$this->getService(?);', [$name]);
     }
 }
示例#2
0
 /**
  * [generateClassType description].
  *
  * @param string      $properties       elementi possibili 'fields', 'extend', 'implements'
  * @param array       $typesReference   [description]
  * @param array       $typesDescription [description]
  * @param ClassConfig $config           [description]
  */
 public function generateClassType(array $properties, $typesReference, $typesDescription, ClassConfig $config)
 {
     $phpNamespace = $this->currentClass->getNamespace();
     if ($config->isInterface) {
         $this->info('Passo a interfaccia', [$this->currentClass->getName()]);
         $docs = $this->currentClass->getComment();
         $this->currentClass = $this->currentFile->addInterface($phpNamespace->getName() . '\\' . ucfirst($this->currentClass->getName()));
         $this->currentClass->setComment($docs);
         $this->info('Check haveConstructor, in caso metto a false', [$config->haveConstructor]);
         $config->haveConstructor = false;
     }
     $this->info('Generate', ['class' => $this->currentClass->getName(), 'namespace' => $phpNamespace->getName(), 'comment' => $this->currentClass->getComment(), 'properties' => $properties]);
     // extend class
     if (array_key_exists('extend', $properties)) {
         $extendClassName = $properties['extend'];
         $this->info('Aggiungo extend', ['class' => $this->currentClass->getName(), 'extend' => $extendClassName]);
         $this->currentClass->setExtends($extendClassName);
         $this->currentClass->getNamespace()->addUse($extendClassName);
     }
     // implements class
     if (array_key_exists('implements', $properties)) {
         $implementsList = [];
         if (!is_array($properties['implements'])) {
             $implementsList[] = $properties['implements'];
         } else {
             $implementsList = array_merge($implementsList, $properties['implements']);
         }
         $this->currentClass->setImplements($implementsList);
         foreach ($implementsList as $implementUse) {
             $this->info('Aggiungo implement', ['class' => $this->currentClass->getName(), 'implements' => $implementUse]);
             $this->currentClass->getNamespace()->addUse($implementUse);
         }
     }
     // traits
     if (array_key_exists('traits', $properties)) {
         if (is_array($properties['traits'])) {
             foreach ($properties['traits'] as $trait) {
                 $this->addTrait($trait, $typesReference);
             }
         } else {
             $traitObject = $properties['traits'];
             $this->addTrait($traitObject, $typesReference);
         }
     }
     if ($config->isFinalClass) {
         $this->currentClass->setFinal(true);
     }
     $first = true;
     if (array_key_exists('fields', $properties)) {
         /** @var $methodConstructor \Nette\PhpGenerator\Method */
         $methodConstructor = null;
         if ($config->haveConstructor) {
             $methodConstructor = $this->addConstructor();
         }
         $body = '';
         foreach ($properties['fields'] as $name => $fieldProperties) {
             $isStatic = false;
             $isAutoinizialize = false;
             $defaultValue = null;
             if (array_key_exists('static', $fieldProperties)) {
                 $isStatic = $fieldProperties['static'];
             }
             if (array_key_exists('autoinizialize', $fieldProperties)) {
                 $isAutoinizialize = boolval($fieldProperties['autoinizialize']);
             }
             if (array_key_exists('default', $fieldProperties)) {
                 $defaultValue = $fieldProperties['default'];
             }
             if (!$isAutoinizialize) {
                 if (null != $defaultValue) {
                     //TODO: usare "primitive type per determinare il corretto IF"
                     //FARE UN TEST PER I BOOLEAN
                     //@see https://www.virendrachandak.com/techtalk/php-isset-vs-empty-vs-is_null/
                     $body .= 'if ( empty($' . $name . ') ) { ' . "\n";
                     if ($isStatic) {
                         $body .= ' self::$';
                     } else {
                         $body .= ' $this->';
                     }
                     $body .= $name . ' = ' . $defaultValue . ';' . "\n";
                     $body .= '} else {';
                     if ($isStatic) {
                         $body .= ' self::$';
                     } else {
                         $body .= ' $this->';
                     }
                     $body .= $name . ' = $' . $name . ';' . "\n";
                     $body .= '}' . "\n";
                 } else {
                     if (!$isStatic) {
                         $body .= ' $this->' . $name . ' = $' . $name . ';' . "\n";
                     }
                 }
             } else {
                 if (!empty($defaultValue) || is_int($defaultValue)) {
                     if (substr(rtrim($defaultValue), -1) == ';') {
                         $this->error('autoinizialize for ' . $name . ' on class ' . $this->currentClass->getName() . ' have default with ";" please remove!');
                         $defaultValue = substr($defaultValue, 0, strlen($defaultValue) - 1);
                     }
                     if (!$isStatic) {
                         if ($isAutoinizialize) {
                             $body .= '// autoinizialize' . "\n";
                             $body .= '$this->' . $name . ' = ' . $defaultValue . ';' . "\n";
                         } else {
                             if ($defaultValue) {
                                 $body .= 'if ( !is_null($' . $name . ') ) {' . "\n";
                                 $body .= ' $this->' . $name . ' = $' . $name . ';' . "\n";
                                 $body .= '} else {' . "\n";
                                 $body .= ' $this->' . $name . ' = ' . $defaultValue . ';' . "\n";
                                 $body .= '}' . "\n";
                                 // $body .= '$this->'.$name.' = '.$defaultValue.';'."\n";
                             } else {
                                 $body .= 'if ( is_null($' . $name . ') ) {' . "\n";
                                 $body .= ' $this->' . $name . ' = ' . $defaultValue . ';' . "\n";
                                 $body .= '}' . "\n";
                             }
                         }
                     }
                 } else {
                     $this->error('autoinizialize for ' . $name . ' not defined on element ' . $this->currentClass->getName());
                     $this->errors[] = 'autoinizialize for ' . $name . ' not defined on element ' . $this->currentClass->getName();
                 }
             }
             $fieldClassFull = '';
             if (array_key_exists('class', $fieldProperties)) {
                 $fieldClassName = ucfirst($fieldProperties['class']);
                 if (array_key_exists($fieldClassName, $typesReference)) {
                     $fieldNamespace = $typesReference[$fieldClassName];
                     $fieldClassFull = $fieldNamespace . '\\' . $fieldClassName;
                     $this->info('Trovato field namespace tra le reference', ['class' => $this->currentClass->getName(), 'field' => $fieldClassName, 'className' => $fieldClassFull]);
                 } else {
                     //FIXME: strpos is better
                     if ($fieldClassName[0] == '\\') {
                         //Class: \DateTime
                         $fieldClassFull = $fieldClassName;
                     } else {
                         $fieldClassFull = $phpNamespace->getName() . '\\' . $fieldClassName;
                         $this->info('Uso class for field same namespace', ['class' => $this->currentClass->getName(), 'field' => $fieldClassName, 'className' => $fieldClassFull]);
                     }
                 }
                 if ($config->haveConstructor && !$isStatic) {
                     $parameter = null;
                     if (!$isAutoinizialize) {
                         $this->info('Aggiungo parametro al costruttore', ['class' => $this->currentClass->getName(), 'parameter' => $name, 'className' => $fieldClassFull, 'default' => $defaultValue, 'autoinizialize' => $isAutoinizialize]);
                         if (!$first) {
                             $parameter = $methodConstructor->addParameter($name, null);
                             //solo i primitivi hanno un default, gli altri null come object
                             $parameter->setTypeHint($fieldClassFull);
                         } else {
                             $parameter = $methodConstructor->addParameter($name);
                             $parameter->setTypeHint($fieldClassFull);
                         }
                     } else {
                         $this->info('Skip parametro al costruttore -> autoinizialize true', ['class' => $this->currentClass->getName(), 'parameter' => $name, 'className' => $fieldClassFull, 'default' => $defaultValue, 'autoinizialize' => $isAutoinizialize]);
                     }
                 }
                 if (array_key_exists($fieldClassName, $typesReference)) {
                     $this->info('Add field type class with namespace', ['class' => $this->currentClass->getName(), 'field' => $fieldClassName, 'className' => $fieldClassFull]);
                     $this->currentClass->getNamespace()->addUse($fieldClassFull);
                 }
             } else {
                 //tipo primitivo
                 $fieldClassName = $fieldProperties['primitive'];
                 $fieldNamespace = null;
                 $fieldClassFull = $fieldProperties['primitive'];
                 if ($config->haveConstructor && !$isStatic) {
                     //FIXME: se sono in php7 ho anche gli altri elementi primitivi
                     //@see: http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration
                     $parameter = null;
                     if (!$isAutoinizialize) {
                         if (is_null($defaultValue)) {
                             $this->info('Aggiungo parametro al costruttore', ['class' => $this->currentClass->getName(), 'parameter' => $name, 'className' => $fieldClassFull, 'default' => $defaultValue, 'autoinizialize' => $isAutoinizialize]);
                             //PHP7 ONLY
                             // if ($fieldClassFull == 'int') {
                             //     $parameter->setTypeHint('int');
                             // }
                             if (!$first) {
                                 $parameter = $methodConstructor->addParameter($name, null);
                             } else {
                                 $parameter = $methodConstructor->addParameter($name);
                             }
                             if ($fieldClassFull == 'array') {
                                 $parameter->setTypeHint('array');
                             } else {
                                 if ($defaultValue != null) {
                                     /* @var $parameter \Nette\PhpGenerator\Parameter */
                                     $parameter->setDefaultValue('' . $defaultValue);
                                 }
                             }
                         }
                     }
                 }
             }
             $this->info('Check autoinizialize field', ['class' => $this->currentClass->getName(), 'field' => $name, 'autoinizialize' => $isAutoinizialize, 'default' => $defaultValue]);
             $comment = 'no description available';
             if (array_key_exists('description', $fieldProperties)) {
                 $comment = $fieldProperties['description'];
             } else {
                 if (!is_null($typesDescription) && array_key_exists($fieldClassName, $typesDescription)) {
                     $comment = $typesDescription[$fieldClassName];
                 }
             }
             if (!$config->isInterface) {
                 /** $field @var \Nette\PhpGenerator\Property */
                 $field = $this->currentClass->addProperty($name);
                 $field->setStatic($isStatic);
                 if ($config->isEnum) {
                     $field->setVisibility('protected');
                 } else {
                     $field->setVisibility('private');
                 }
                 $field->addComment($comment)->addComment('@var ' . $fieldClassFull);
             }
             $createSetter = $config->haveSetter;
             if (array_key_exists('setter', $fieldProperties)) {
                 $createSetter = $fieldProperties['setter'];
             }
             $createGetter = $config->haveGetter;
             if (array_key_exists('getter', $fieldProperties)) {
                 $createGetter = $fieldProperties['getter'];
             }
             if ($config->isInterface) {
                 if ($createGetter) {
                     $this->addGetter($name, $fieldClassFull, $isStatic, false);
                 }
                 if ($createSetter) {
                     $this->addSetter($name, $fieldClassFull, $isStatic, false);
                 }
             } else {
                 if ($createGetter) {
                     $this->addGetter($name, $fieldClassFull, $isStatic, true);
                 }
                 if ($createSetter) {
                     $this->addSetter($name, $fieldClassFull, $isStatic, true);
                 }
             }
             if (!$isAutoinizialize) {
                 $first = false;
             }
         }
         if ($config->haveConstructor) {
             $methodConstructor->setBody($body, []);
         }
     }
     //end fields
     if (array_key_exists('methods', $properties)) {
         $body = '';
         foreach ($properties['methods'] as $methodName => $methodsProperties) {
             $this->info('Aggiungo method', ['class' => $this->currentClass->getName(), 'methodName' => $methodName, 'methodProp' => $methodsProperties]);
             /** $newMethodCall @var \Nette\PhpGenerator\Method */
             $newMethodCall = $this->currentClass->addMethod($methodName);
             $newMethodCall->setFinal(true);
             $newMethodCall->setStatic(false);
             if (array_key_exists('static', $methodsProperties)) {
                 $newMethodCall->setStatic($methodsProperties['static']);
             }
             if (array_key_exists('description', $methodsProperties)) {
                 $newMethodCall->setVisibility($methodsProperties['visibility']);
             } else {
                 $newMethodCall->setVisibility('public');
             }
             if (array_key_exists('description', $methodsProperties)) {
                 $newMethodCall->addComment($methodsProperties['description']);
             } else {
                 $returnType = 'void';
                 if (array_key_exists('@return', $methodsProperties)) {
                     $returnType = $methodsProperties['@return'];
                     //TODO: .'|null' va messo in quale condizione?
                     $newMethodCall->addComment('@return ' . $returnType);
                 } else {
                     //NOPE
                 }
             }
             if (array_key_exists('params', $methodsProperties)) {
                 foreach ($methodsProperties['params'] as $paramName => $paramProp) {
                     if (array_key_exists('class', $paramProp)) {
                         $newMethodCall->addParameter($paramName)->setTypeHint($paramProp['class']);
                     }
                     if (array_key_exists('primitive', $paramProp)) {
                         $newMethodCall->addParameter($paramName);
                     }
                 }
             }
             $body = ' // FIMXE: da implementare ';
             if (array_key_exists('body', $methodsProperties)) {
                 $body = $methodsProperties['body'];
             }
             $newMethodCall->setBody($body);
         }
     }
     if ($config->isEnum) {
         $this->currentClass->setAbstract(true);
         $this->addSingleton('Singleton instance for enum', false);
         $this->addParseString();
     }
     if ($config->isSingleton) {
         $this->addSingleton('Singleton instance', true);
     }
 }
示例#3
0
    public function generateTests($outputFolder)
    {
        $container = \Testbench\ContainerFactory::create(FALSE);
        $presenterFactory = $container->getByType('Nette\\Application\\IPresenterFactory');
        $presenters = $container->findByType('Nette\\Application\\UI\\Presenter');
        foreach ($presenters as $presenter) {
            $this->renderMethods = $this->handleMethods = $this->componentMethods = [];
            /** @var \Nette\Application\UI\Presenter $service */
            $service = $container->getService($presenter);
            if ($service instanceof \Testbench\Mocks\PresenterMock) {
                continue;
            }
            if ($service instanceof \KdybyModule\CliPresenter) {
                //Oh, Kdyby! :-(
                continue;
            }
            $rc = new \ReflectionClass($service);
            $renderPrefix = $service->formatActionMethod('') . '|' . $service->formatRenderMethod('');
            $methods = $rc->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED);
            foreach ($methods as $method) {
                $methodName = $method->getName();
                if (preg_match("~^({$renderPrefix})[a-z0-9]+~i", $methodName)) {
                    try {
                        $optionalArgs = $this->tryCall($service, $methodName, $service->getParameters(), TRUE);
                        if (preg_match('~.*rss.*~i', $methodName)) {
                            $this->renderMethods[$methodName] = 'rss';
                        } elseif (preg_match('~.*sitemap.*~i', $methodName)) {
                            $this->renderMethods[$methodName] = 'sitemap';
                        } else {
                            $requiredArgs = $this->tryCall($service, $methodName, $service->getParameters(), FALSE);
                            $this->renderMethods[$methodName] = ['action', [$optionalArgs, $requiredArgs]];
                        }
                    } catch (\Nette\Application\AbortException $exc) {
                        $this->renderMethods[$methodName] = ['action', $this->getResponse($service)];
                    } catch (\Exception $exc) {
                        $this->renderMethods[$methodName] = ['exception', $exc];
                    }
                }
                if (preg_match('~^handle[a-z0-9]+~i', $methodName)) {
                    if ($methodName === 'handleInvalidLink') {
                        //internal method
                        continue;
                    }
                    $this->handleMethods[] = $methodName;
                }
                if (preg_match('~^createComponent[a-z0-9]+~i', $methodName)) {
                    $method->setAccessible(TRUE);
                    $form = $method->invoke($service);
                    if ($form instanceof \Nette\Application\UI\Form) {
                        $this->componentMethods[$methodName] = $form;
                    }
                }
            }
            $testClassName = $rc->getShortName() . 'Test';
            $testClass = new PhpGenerator\ClassType($testClassName);
            $testClass->setExtends('\\Tester\\TestCase');
            $testClass->addTrait('\\Testbench\\TPresenter');
            $testClass->addComment('@testCase');
            foreach ($this->renderMethods as $testMethod => $testMethodType) {
                $generatedMethod = $testClass->addMethod('test' . ucfirst($testMethod));
                $destination = $presenterFactory->unformatPresenterClass($rc->getName()) . ':';
                $destination .= lcfirst(preg_replace('~^(action|render)([a-z]+)~i', '$2', $testMethod));
                $extra = NULL;
                if (is_array($testMethodType)) {
                    /** @var \Exception|\Nette\Application\IResponse $extra */
                    $extra = $testMethodType[1];
                    $testMethodType = $testMethodType[0];
                    //FIXME: fuj, hnus
                }
                switch ($testMethodType) {
                    case 'rss':
                        $generatedMethod->addBody('$this->checkRss(?);', [$destination]);
                        break;
                    case 'sitemap':
                        $generatedMethod->addBody('$this->checkSitemap(?);', [$destination]);
                        break;
                    case 'action':
                        if ($extra instanceof \Nette\Application\Responses\RedirectResponse) {
                            $url = new \Nette\Http\Url($extra->getUrl());
                            $generatedMethod->addBody('$this->checkRedirect(?, ?);', [$destination, $url->getPath()]);
                        } elseif ($extra instanceof \Nette\Application\Responses\JsonResponse) {
                            $generatedMethod->addBody('$this->checkJson(?);', [$destination]);
                        } else {
                            if ($extra[0]) {
                                $generatedMethod->addBody('//FIXME: parameters may not be correct');
                                $generatedMethod->addBody("\$this->checkAction(?, ?);\n", [$destination, $extra[0]]);
                                $generatedMethod->addBody('$this->checkAction(?, ?);', [$destination, $extra[1]]);
                            } else {
                                $generatedMethod->addBody('$this->checkAction(?);', [$destination]);
                            }
                        }
                        break;
                    case 'exception':
                        $this->generateExceptionBody($generatedMethod, $destination, $extra);
                        break;
                }
            }
            foreach ($this->handleMethods as $testMethod) {
                $destination = $presenterFactory->unformatPresenterClass($rc->getName());
                $action = lcfirst(preg_replace('~^handle([a-z]+)~i', '$1', $testMethod));
                $testClass->addMethod('test' . ucfirst($testMethod))->addBody('$this->checkSignal(?, ?);', [$destination . ':', $action]);
            }
            foreach ($this->componentMethods as $testMethod => $form) {
                $destination = $presenterFactory->unformatPresenterClass($rc->getName());
                $action = lcfirst(preg_replace('~^createComponent([a-z]+)~i', '$1', $testMethod));
                $controls = '';
                /** @var \Nette\Application\UI\Form $form */
                foreach ($form->getControls() as $control) {
                    if ($control->getName() === '_token_' || $control instanceof \Nette\Forms\Controls\SubmitButton) {
                        continue;
                    }
                    $value = "'###', //FIXME: replace with value";
                    if ($control instanceof \Nette\Forms\Controls\Checkbox) {
                        $value = 'FALSE';
                    }
                    $controls .= "\t'" . $control->getName() . "' => {$value}\n";
                }
                try {
                    $form->onSuccess($form, $form->getValues());
                    $testClass->addMethod('test' . ucfirst($testMethod))->addBody("\$this->checkForm(?, ?, [\n" . $controls . '], ?);', [$destination . ':', $action, FALSE]);
                } catch (\Nette\Application\AbortException $exc) {
                    $extra = $this->getResponse($service);
                    $path = $extra ? (new \Nette\Http\Url($extra->getUrl()))->getPath() : '/';
                    $testClass->addMethod('test' . ucfirst($testMethod))->addBody("\$this->checkForm(?, ?, [\n" . $controls . '], ?);', [$destination . ':', $action, $path]);
                } catch (\Exception $exc) {
                    //This sucks but we have to move on - failure is not an option
                }
            }
            $namespace = $rc->getNamespaceName();
            $namespace = $namespace ? '\\' . $namespace : '';
            $generatedTest = "<?php\n\nnamespace Tests{$namespace};\n\nuse Tester\\Assert;\n\n";
            $depth = substr_count($namespace, '\\');
            $levelsUp = str_repeat('../', $depth);
            $generatedTest .= "require __DIR__ . '/{$levelsUp}bootstrap.php';\n\n";
            $generatedTest .= $testClass;
            $generatedTest .= "\n(new {$testClassName})->run();\n";
            $testFileName = preg_replace('~\\\\~', DIRECTORY_SEPARATOR, get_class($service));
            \Nette\Utils\FileSystem::write($outputFolder . '/' . $testFileName . '.phpt', $generatedTest);
            \Nette\Utils\FileSystem::createDir($outputFolder . '/_temp');
            \Nette\Utils\FileSystem::write($outputFolder . '/tests.neon', <<<'NEON'
# WARNING: it is CRITICAL that this file & directory are NOT accessible directly via a web browser!
# http://nette.org/security-warning

# (this is just an example)

routing:
	routes:
		'/x/y[[[/<presenter>]/<action>][/<id>]]': 'Presenter:default'

NEON
);
        }
    }
 /**
  * @param $className
  */
 private function createMapper($className)
 {
     $postfix = 'Mapper';
     $class = new ClassType($className . $postfix, $this->namespace);
     $class->setExtends('Nextras\\Orm\\Mapper\\Mapper');
     $this->createClass($class, $className);
 }
示例#5
0
 /**
  * @param string $tableName
  * @param array $columns
  * @return void
  */
 protected function generateTableGeneratedHyperRow($tableName, $columns)
 {
     $classFqn = $this->config['classes']['row']['generated'];
     $classFqn = Helpers::substituteClassWildcard($classFqn, $tableName);
     $className = Helpers::extractClassName($classFqn);
     $classNamespace = Helpers::extractNamespace($classFqn);
     $extendsFqn = $this->config['classes']['row']['base'];
     $extends = Helpers::formatClassName($extendsFqn, $classNamespace);
     $class = new ClassType($className);
     $class->setExtends($extends);
     // Add property annotations based on columns
     foreach ($columns as $column => $type) {
         if (in_array($type, [IStructure::FIELD_DATETIME, IStructure::FIELD_TIME, IStructure::FIELD_DATE, IStructure::FIELD_UNIX_TIMESTAMP])) {
             $type = '\\Nette\\Utils\\DateTime';
         }
         $class->addComment("@property-read {$type} \${$column}");
     }
     // Generate methods.row.getter
     foreach ((array) $this->config['methods']['row']['getter'] as $methodTemplate) {
         // Generate column getters
         foreach ($columns as $column => $type) {
             if (in_array($type, [IStructure::FIELD_DATETIME, IStructure::FIELD_TIME, IStructure::FIELD_DATE, IStructure::FIELD_UNIX_TIMESTAMP])) {
                 $type = '\\Nette\\Utils\\DateTime';
             }
             $methodName = Helpers::substituteMethodWildcard($methodTemplate, $column);
             $returnType = $type;
             $class->addMethod($methodName)->addBody('return $this->activeRow->?;', [$column])->addComment("@return {$returnType}");
             // Add property annotation
             if (Strings::startsWith($methodName, 'get')) {
                 $property = Strings::firstLower(Strings::substring($methodName, 3));
                 if ($property != $column) {
                     $class->addComment("@property-read {$type} \${$property}");
                 }
             }
         }
     }
     // Generate methods.row.ref
     foreach ((array) $this->config['methods']['row']['ref'] as $methodTemplate) {
         // Generate 'ref' methods
         foreach ($this->structure->getBelongsToReference($tableName) as $referencingColumn => $referencedTable) {
             if (is_array($this->config['tables']) && !in_array($referencedTable, $this->config['tables'])) {
                 continue;
             }
             $result = Helpers::underscoreToCamelWithoutPrefix(Strings::replace($referencingColumn, '~_id$~'), $tableName);
             $methodName = Helpers::substituteMethodWildcard($methodTemplate, $result);
             $returnType = $this->getTableClass('row', $referencedTable, $classNamespace);
             $class->addMethod($methodName)->addBody('return $this->ref(?, ?);', [$referencedTable, $referencingColumn])->addComment("@return {$returnType}");
             // Add property annotations
             if (Strings::startsWith($methodName, 'get')) {
                 $property = Strings::firstLower(Strings::substring($methodName, 3));
                 $class->addComment("@property-read {$returnType} \${$property}");
             }
         }
     }
     // Generate methods.row.related
     foreach ((array) $this->config['methods']['row']['related'] as $methodTemplate) {
         // Generate 'related' methods
         foreach ($this->structure->getHasManyReference($tableName) as $relatedTable => $referencingColumns) {
             if (is_array($this->config['tables']) && !in_array($relatedTable, $this->config['tables'])) {
                 continue;
             }
             foreach ($referencingColumns as $referencingColumn) {
                 // Omit longest common prefix between $relatedTable and (this) $tableName
                 $result = Helpers::underscoreToCamelWithoutPrefix($relatedTable, $tableName);
                 if (count($referencingColumns) > 1) {
                     $suffix = 'As' . Helpers::underscoreToCamel(Strings::replace($referencingColumn, '~_id$~'));
                 } else {
                     $suffix = NULL;
                 }
                 $methodName = Helpers::substituteMethodWildcard($methodTemplate, $result, $suffix);
                 $returnType = $this->getTableClass('selection', $relatedTable, $classNamespace);
                 $class->addMethod($methodName)->addBody('return $this->related(?, ?);', [$relatedTable, $referencingColumn])->addComment("@return {$returnType}");
                 // Add property annotations
                 if (Strings::startsWith($methodName, 'get')) {
                     $property = Strings::firstLower(Strings::substring($methodName, 3));
                     $class->addComment("@property-read {$returnType} \${$property}");
                 }
             }
         }
     }
     $code = implode("\n\n", ['<?php', "/**\n * This is a generated file. DO NOT EDIT. It will be overwritten.\n */", "namespace {$classNamespace};", $class]);
     $dir = $this->config['dir'] . '/' . 'tables' . '/' . $tableName;
     $file = $dir . '/' . $className . '.php';
     $this->writeIfChanged($file, $code);
 }