Codestarter: fase 3 update
In my previous post I mentioned extendibility would be a future step. I threw that idea overboard. So now I'm doing the clean up while adding extendibility. How to make a console command extendible I already figured out in the pervious post that the extendibility had to be in the question flow. This lead me to the following command code. // in the execute method $flows = $this->getFlows($this->fs, $input, $output, $helper); $typeQuestion = new ChoiceQuestion('Which file type do you want to create?', array_keys($flows)); $typeAnswer = $helper->ask($input, $output, $typeQuestion); $newFile = $flows[$typeAnswer]->generate($typeAnswer, $fileNameAnswer); /** * visual separation **/ private function getFlows(Filesystem $filesystem, InputInterface $input, OutputInterface $output, HelperInterface $helper) : array { $typesAndClasses = []; $classes = [ TypeFlow::class ]; $customClasses = getenv('CODESTARTER_CUSTOM_FLOWS'); $classes = array_merge($classes, explode(',', $customClasses)); foreach ($classes as $class) { $instance = new $class($filesystem, $input, $output, $helper); $types = $instance->types; foreach ($types as $type) { $typesAndClasses[$class] = $instance; } } return $typesAndClasses; } I created an AbstractFlow class. abstract class AbstractFlow { /** * Add the types that can be selected when running the codestarter command. * The types class, trait, interface and enum are added by default. * The types can be overridden by adding a type with the same name to a custom flow. * * @var array */ public array $types = []; public function __construct( protected Filesystem $filesystem, protected InputInterface $input, protected OutputInterface $output, protected HelperInterface $helper ) { } /** * Override this method in the custom flow class * * @param string $type * @param string $fileName * @return NewFileDTO */ public function generate(string $type, string $fileName): NewFileDTO { $this->output->writeln('The flow function is not implemented.'); return new NewFileDTO('',''); } } I documented this class very thoroughly because this is the class other developers are going to use. The TypeFlow class extends the AbstractFlow class. This allows me to add helper methods to the AbstractFlow class. Mainly these are question helpers. At the moment I used a CODESTARTER_CUSTOM_FLOWS environment variable to identify the classes in a project. Later I'm going to replace it with a discovery mechanism. This way developers have full control over their question flows, template objects and templates. Clean up progress In the generate method of the TypeFlow class I added the things that are common and the things that are different I placed in a separate method. public function generate(string $type, string $fileName): NewFileDTO { $content = new Content(); $content->type = $type; $content->typeName = $fileName; $content->namespace = $this->getNamespaceFromQuestion('Add the namespace'); $this->{$type . 'Flow'}($content); return new NewFileDTO('', $content->getGeneratedContent()); } The classFlow method is almost complete. private function classFlow(Content &$content) : void { if($this->getConfirmationFromQuestion('Has the class attributes?')) { $content->typeAttributes = $this->getAttributesFromQuestion( 'What is the attribute class?', 'Does the attribute have arguments?', 'What are the arguments?', ); } if($this->getConfirmationFromQuestion('Is the class abstract?')) { $content->typePrefixes[] = 'abstract'; } if($this->getConfirmationFromQuestion('Is the class final?')) { $content->typePrefixes[] = 'final'; } if($this->getConfirmationFromQuestion('Is the class readonly?')) { $content->typePrefixes[] = 'readonly'; } if($this->getConfirmationFromQuestion('Has the class a parent?')) { $extendAnswer = $this->getClassFromQuestion('What is the parent type?'); $content->imports[] = new ImportDTO($extendAnswer, Type::Object); $content->typeExtend = $this->getClassFromClasspath($extendAnswer); } if($this->getConfirmationFromQuestion('Has the class interfaces?')) { $interfacesAnswer = $this->getClassesFromQuestion('What is the interface type?'); $interfaces = array_map(fn($import) => new ImportDTO($import, Type::Interface), array_keys($interfacesAnswer)); $content->imports = array_merge($content->imports, $interfaces); $content->typeIm

In my previous post I mentioned extendibility would be a future step. I threw that idea overboard. So now I'm doing the clean up while adding extendibility.
How to make a console command extendible
I already figured out in the pervious post that the extendibility had to be in the question flow.
This lead me to the following command code.
// in the execute method
$flows = $this->getFlows($this->fs, $input, $output, $helper);
$typeQuestion = new ChoiceQuestion('Which file type do you want to create?',
array_keys($flows));
$typeAnswer = $helper->ask($input, $output, $typeQuestion);
$newFile = $flows[$typeAnswer]->generate($typeAnswer, $fileNameAnswer);
/**
* visual separation
**/
private function getFlows(Filesystem $filesystem, InputInterface $input, OutputInterface $output, HelperInterface $helper) : array
{
$typesAndClasses = [];
$classes = [
TypeFlow::class
];
$customClasses = getenv('CODESTARTER_CUSTOM_FLOWS');
$classes = array_merge($classes, explode(',', $customClasses));
foreach ($classes as $class) {
$instance = new $class($filesystem, $input, $output, $helper);
$types = $instance->types;
foreach ($types as $type) {
$typesAndClasses[$class] = $instance;
}
}
return $typesAndClasses;
}
I created an AbstractFlow
class.
abstract class AbstractFlow
{
/**
* Add the types that can be selected when running the codestarter command.
* The types class, trait, interface and enum are added by default.
* The types can be overridden by adding a type with the same name to a custom flow.
*
* @var array
*/
public array $types = [];
public function __construct(
protected Filesystem $filesystem,
protected InputInterface $input,
protected OutputInterface $output,
protected HelperInterface $helper
)
{
}
/**
* Override this method in the custom flow class
*
* @param string $type
* @param string $fileName
* @return NewFileDTO
*/
public function generate(string $type, string $fileName): NewFileDTO {
$this->output->writeln('The flow function is not implemented. ');
return new NewFileDTO('','');
}
}
I documented this class very thoroughly because this is the class other developers are going to use.
The TypeFlow
class extends the AbstractFlow
class. This allows me to add helper methods to the AbstractFlow
class. Mainly these are question helpers.
At the moment I used a CODESTARTER_CUSTOM_FLOWS environment variable to identify the classes in a project. Later I'm going to replace it with a discovery mechanism.
This way developers have full control over their question flows, template objects and templates.
Clean up progress
In the generate
method of the TypeFlow
class I added the things that are common and the things that are different I placed in a separate method.
public function generate(string $type, string $fileName): NewFileDTO
{
$content = new Content();
$content->type = $type;
$content->typeName = $fileName;
$content->namespace = $this->getNamespaceFromQuestion('Add the namespace');
$this->{$type . 'Flow'}($content);
return new NewFileDTO('', $content->getGeneratedContent());
}
The classFlow
method is almost complete.
private function classFlow(Content &$content) : void {
if($this->getConfirmationFromQuestion('Has the class attributes?')) {
$content->typeAttributes = $this->getAttributesFromQuestion(
'What is the attribute class?',
'Does the attribute have arguments?',
'What are the arguments?',
);
}
if($this->getConfirmationFromQuestion('Is the class abstract?')) {
$content->typePrefixes[] = 'abstract';
}
if($this->getConfirmationFromQuestion('Is the class final?')) {
$content->typePrefixes[] = 'final';
}
if($this->getConfirmationFromQuestion('Is the class readonly?')) {
$content->typePrefixes[] = 'readonly';
}
if($this->getConfirmationFromQuestion('Has the class a parent?')) {
$extendAnswer = $this->getClassFromQuestion('What is the parent type?');
$content->imports[] = new ImportDTO($extendAnswer, Type::Object);
$content->typeExtend = $this->getClassFromClasspath($extendAnswer);
}
if($this->getConfirmationFromQuestion('Has the class interfaces?')) {
$interfacesAnswer = $this->getClassesFromQuestion('What is the interface type?');
$interfaces = array_map(fn($import) => new ImportDTO($import, Type::Interface), array_keys($interfacesAnswer));
$content->imports = array_merge($content->imports, $interfaces);
$content->typeImplements = array_values($interfacesAnswer);
}
if($this->getConfirmationFromQuestion('Has the class methods?')) {
$methodsAnswer = $this->getMethodsFromQuestion(
'What is the method name?',
'Has the method attributes?',
'What is the atribute class?',
'Has the attribute arguements?',
'What are the attribute arguments?',
'What is the method visibility?',
'Is the method static?',
'has the method arguments?',
'What is the arguement visibility?',
'Is the argument readonly?',
'Has the argument a type?',
'Is it a single type?',
'Is the type a scalar?',
'What is the scalar?',
'What class is the type?',
'What is the name of the argument?',
'What is the default of the argument?',
'Do you want to add the body of the method?',
'What is the body?',
);
foreach($methodsAnswer as $method) {
foreach($method->arguments as $argument) {
if(strlen($argument->import) > 0) {
$content->imports[] = $argument->import;
}
}
if(strlen($method->returnTypeImport) > 0) {
$content->imports[] = $method->returnTypeImport;
}
}
$content->methods = $methodsAnswer;
}
}
It is quite a flow because there are a lot of questions to ask to create code. I found it amazing how many decisions we make just to bootstrap a class.
Another pattern I didn't realise before is that the construtor promotion is so close to method arguments. I knew it in the back of my mind, but setting up a method made me realise this as an eureka moment. That was funny.
The directories are cleaned up a bit.
bin/
src/
├── bin/
├── Console/
├── Content/
│ ├── Php/
│ │ ├── DTO/
templates/
├── php/
tests/
One of my goals is to generate all kinds of code files, not only PHP files. And I realised the flow at the moment is only for PHP files.
So it is likely the AbstractFlow
class wil have a parent class, to create type specific helper methods.
Next steps
Because I had to figure out the extendibility and question flow I have written no tests. So that will be the next priority.
After I have an acceptable code coverage, I will move on to the other flows. Those will be easier because I did a lot of work with the class flow.
The final step in fase 3 is publishing the package.