├── Classes
├── Command
│ ├── AbstractCommand.php
│ ├── Component
│ │ ├── BackendControllerCommand.php
│ │ ├── CommandCommand.php
│ │ ├── EventListenerCommand.php
│ │ ├── MiddlewareCommand.php
│ │ ├── SimpleComponentCommand.php
│ │ └── TestingSetupCommand.php
│ └── ExtensionCommand.php
├── Component
│ ├── AbstractComponent.php
│ ├── ArrayConfigurationComponentInterface.php
│ ├── BackendController.php
│ ├── Command.php
│ ├── ComponentInterface.php
│ ├── EventListener.php
│ ├── Extension.php
│ ├── Middleware.php
│ ├── ServiceConfigurationComponentInterface.php
│ └── TestingSetup.php
├── Environment
│ └── Variables.php
├── Exception
│ ├── AbortCommandException.php
│ ├── EmptyAnswerException.php
│ ├── InvalidPackageException.php
│ └── InvalidPackageNameException.php
├── IO
│ ├── AbstractConfiguration.php
│ ├── ArrayConfiguration.php
│ ├── ConfigurationInterface.php
│ └── ServiceConfiguration.php
└── PackageResolver.php
├── Configuration
└── Services.yaml
├── LICENSE
├── README.md
├── Resources
├── Private
│ └── CodeTemplates
│ │ ├── BackendController.php
│ │ ├── Build
│ │ ├── Scripts
│ │ │ └── runTests.sh
│ │ └── testing-docker
│ │ │ └── docker-compose.yml
│ │ ├── Command.php
│ │ ├── EventListener.php
│ │ └── Middleware.php
└── Public
│ └── Icons
│ └── Extension.svg
├── composer.json
└── ext_emconf.php
/Classes/Command/AbstractCommand.php:
--------------------------------------------------------------------------------
1 | io = new SymfonyStyle($input, $output);
40 | $this->packageResolver = GeneralUtility::makeInstance(PackageResolver::class);
41 | }
42 |
43 | protected function getProposalFromEnvironment(string $key, string $default = ''): string
44 | {
45 | return Variables::has($key) ? Variables::get($key) : $default;
46 | }
47 |
48 | /**
49 | * @param mixed|string $answer
50 | */
51 | public function answerRequired($answer): string
52 | {
53 | $answer = (string)$answer;
54 |
55 | if (trim($answer) === '') {
56 | throw new EmptyAnswerException('Answer can not be empty.', 1639664759);
57 | }
58 |
59 | return $answer;
60 | }
61 |
62 | /**
63 | * @param mixed|string $answer
64 | *
65 | * @see https://getcomposer.org/doc/04-schema.md#name
66 | */
67 | public function validatePackageKey($answer): string
68 | {
69 | $answer = $this->answerRequired($answer);
70 |
71 | if (!preg_match('/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$/', $answer)) {
72 | throw new InvalidPackageNameException(
73 | 'Package key does not match the allowed pattern. More information are available on https://getcomposer.org/doc/04-schema.md#name.',
74 | 1639664760
75 | );
76 | }
77 |
78 | return $answer;
79 | }
80 |
81 | /**
82 | * Resolve package using the extension key from either input argument, environment variable or CLI
83 | */
84 | protected function getPackage(InputInterface $input): ?PackageInterface
85 | {
86 | if ($input->hasArgument('extensionKey')
87 | && ($key = ($input->getArgument('extensionKey') ?? '')) !== ''
88 | ) {
89 | return $this->packageResolver->resolvePackage($key);
90 | }
91 |
92 | if (($key = $this->getProposalFromEnvironment('EXTENSION_KEY')) !== '') {
93 | return $this->packageResolver->resolvePackage($key);
94 | }
95 |
96 | if (($key = $this->askForExtensionKey()) !== '') {
97 | $this->io->note('You can also always set the extension key as argument or by using an environment variable.');
98 | return $this->packageResolver->resolvePackage($key);
99 | }
100 |
101 | return null;
102 | }
103 |
104 | /**
105 | * Let user select an extension to work with.
106 | */
107 | protected function askForExtensionKey(): string
108 | {
109 | $packages = $this->packageResolver->getAvailablePackages();
110 | $choices = array_reduce($packages, static function ($result, PackageInterface $package) {
111 | if ($package->getValueFromComposerManifest('type') === 'typo3-cms-extension' && $package->getPackageKey() !== 'make') {
112 | $extensionKey = $package->getPackageKey();
113 | $result[$extensionKey] = $extensionKey;
114 | }
115 | return $result;
116 | }, []);
117 |
118 | if (!$choices) {
119 | throw new \LogicException('No available extension found. You may want to execute "bin/typo3 make:extension".');
120 | }
121 |
122 | return (string)$this->io->choice('Select a extension to work on', $choices);
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Classes/Command/Component/BackendControllerCommand.php:
--------------------------------------------------------------------------------
1 | setDescription('Create a backend controller');
30 | }
31 |
32 | protected function initialize(InputInterface $input, OutputInterface $output): void
33 | {
34 | parent::initialize($input, $output);
35 | $this->initializeServiceConfiguration();
36 | $this->initializeArrayConfiguration('Routes.php', 'Configuration/Backend/');
37 | }
38 |
39 | protected function createComponent(): ComponentInterface
40 | {
41 | $backendController = new BackendController($this->psr4Prefix);
42 | return $backendController
43 | ->setName(
44 | (string)$this->io->ask(
45 | 'Enter the name of the backend controller (e.g. "AwesomeController")',
46 | null,
47 | [$this, 'answerRequired']
48 | )
49 | )
50 | ->setDirectory(
51 | (string)$this->io->ask(
52 | 'Enter the directory, the backend controller should be placed in',
53 | $this->getProposalFromEnvironment('BACKEND_CONTROLLER_DIR', 'Backend/Controller')
54 | )
55 | )
56 | ->setRouteIdentifier(
57 | (string)$this->io->ask(
58 | 'Enter the route identifier for the backend controller',
59 | $backendController->getRouteIdentifierProposal($this->getProposalFromEnvironment('BACKEND_CONTROLLER_PREFIX', $this->extensionKey))
60 | )
61 | )
62 | ->setRoutePath(
63 | (string)$this->io->ask(
64 | 'Enter the route path of the backend controller?',
65 | $backendController->getRoutePathProposal()
66 | )
67 | )
68 | ->setMethodName(
69 | (string)$this->io->ask('Enter the method, which should handle the request - LEAVE EMPTY FOR USING __invoke()')
70 | );
71 | }
72 |
73 | /**
74 | * @param BackendController $component
75 | * @throws AbortCommandException
76 | */
77 | protected function publishComponentConfiguration(ComponentInterface $component): bool
78 | {
79 | if (!$this->writeServiceConfiguration($component)) {
80 | $this->io->error('Updating the service configuration failed.');
81 | return false;
82 | }
83 |
84 | $routeConfiguration = $this->arrayConfiguration->getConfiguration();
85 | if (isset($routeConfiguration[$component->getRouteIdentifier()])
86 | && !$this->io->confirm('The route identifier ' . $component->getRouteIdentifier() . ' already exists. Do you want to override it?', true)
87 | ) {
88 | throw new AbortCommandException('Aborting backend controller generation.', 1639664754);
89 | }
90 |
91 | $routeConfiguration[$component->getRouteIdentifier()] = $component->getArrayConfiguration();
92 | $this->arrayConfiguration->setConfiguration($routeConfiguration);
93 | if (!$this->writeArrayConfiguration()) {
94 | $this->io->error('Updating the routing configuration failed.');
95 | return false;
96 | }
97 |
98 | $this->io->success('Successfully created the backend controller ' . $component->getName() . ' (' . $component->getRouteIdentifier() . ').');
99 | return true;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Classes/Command/Component/CommandCommand.php:
--------------------------------------------------------------------------------
1 | setDescription('Create a console command');
30 | }
31 |
32 | protected function initialize(InputInterface $input, OutputInterface $output): void
33 | {
34 | parent::initialize($input, $output);
35 | $this->initializeServiceConfiguration();
36 | }
37 |
38 | protected function createComponent(): ComponentInterface
39 | {
40 | $command = new Command($this->psr4Prefix);
41 | return $command
42 | ->setName(
43 | (string)$this->io->ask(
44 | 'Enter the name of the command (e.g. "AwesomeCommand")?',
45 | null,
46 | [$this, 'answerRequired']
47 | )
48 | )
49 | ->setDirectory(
50 | (string)$this->io->ask(
51 | 'Enter the directory, the command should be placed in',
52 | $this->getProposalFromEnvironment('COMMAND_DIR', 'Command')
53 | )
54 | )
55 | ->setCommandName(
56 | (string)$this->io->ask(
57 | 'Enter the command name to execute on CLI',
58 | $command->getCommandNameProposal($this->getProposalFromEnvironment('COMMAND_NAME_PREFIX', $this->extensionKey))
59 | )
60 | )
61 | ->setDescription(
62 | (string)$this->io->ask(
63 | 'Enter a short description for the command',
64 | null,
65 | [$this, 'answerRequired']
66 | )
67 | )
68 | ->setSchedulable(
69 | (bool)$this->io->confirm('Should the command be schedulable?', false)
70 | );
71 | }
72 |
73 | /**
74 | * @param Command $component
75 | * @throws AbortCommandException
76 | */
77 | protected function publishComponentConfiguration(ComponentInterface $component): bool
78 | {
79 | if (!$this->writeServiceConfiguration($component)) {
80 | $this->io->error('Updating the service configuration failed.');
81 | return false;
82 | }
83 |
84 | $this->io->success('Successfully created the command ' . $component->getName() . ' (' . $component->getCommandName() . ').');
85 | return true;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Classes/Command/Component/EventListenerCommand.php:
--------------------------------------------------------------------------------
1 | setDescription('Create a PSR-14 event listener');
30 | }
31 |
32 | protected function initialize(InputInterface $input, OutputInterface $output): void
33 | {
34 | parent::initialize($input, $output);
35 | $this->initializeServiceConfiguration();
36 | }
37 |
38 | protected function createComponent(): ComponentInterface
39 | {
40 | $eventListener = new EventListener($this->psr4Prefix);
41 | return $eventListener
42 | ->setEventName(
43 | (string)$this->io->ask(
44 | 'Enter the event to listen for? - Use the FQN',
45 | null,
46 | [$this, 'answerRequired']
47 | )
48 | )
49 | ->setName(
50 | (string)$this->io->ask(
51 | 'Enter the name of the listener (e.g. "AwesomeEventListener")',
52 | $eventListener->getNameProposal()
53 | )
54 | )
55 | ->setDirectory(
56 | (string)$this->io->ask(
57 | 'Enter the directory, the listener should be placed in',
58 | $this->getProposalFromEnvironment('EVENT_LISTENER_DIR', 'EventListener')
59 | )
60 | )
61 | ->setIdentifier(
62 | (string)$this->io->ask(
63 | 'Enter an identifier for the listener',
64 | $eventListener->getIdentifierProposal($this->getProposalFromEnvironment('EVENT_LISTENER_IDENTIFIER_PREFIX'))
65 | )
66 | )
67 | ->setMethodName(
68 | (string)$this->io->ask('Enter the method, which should receive the event - LEAVE EMPTY FOR USING __invoke()')
69 | );
70 | }
71 |
72 | /**
73 | * @param EventListener $component
74 | * @throws AbortCommandException
75 | */
76 | protected function publishComponentConfiguration(ComponentInterface $component): bool
77 | {
78 | if (!$this->writeServiceConfiguration($component)) {
79 | $this->io->error('Updating the service configuration failed.');
80 | return false;
81 | }
82 |
83 | $this->io->success('Successfully created the event listener ' . $component->getName() . ' for event ' . $component->getEventName());
84 | return true;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Classes/Command/Component/MiddlewareCommand.php:
--------------------------------------------------------------------------------
1 | setDescription('Create a PSR-15 middleware');
31 | }
32 |
33 | protected function initialize(InputInterface $input, OutputInterface $output): void
34 | {
35 | parent::initialize($input, $output);
36 | $this->initializeArrayConfiguration('RequestMiddlewares.php');
37 | }
38 |
39 | protected function createComponent(): ComponentInterface
40 | {
41 | $middleware = new Middleware($this->psr4Prefix);
42 | return $middleware
43 | ->setName(
44 | (string)$this->io->ask(
45 | 'Enter the name of the middleware (e.g. "PostProcessContent")',
46 | null,
47 | [$this, 'answerRequired']
48 | )
49 | )
50 | ->setDirectory(
51 | (string)$this->io->ask(
52 | 'Enter the directory, the middleware should be placed in',
53 | $this->getProposalFromEnvironment('MIDDLEWARE_DIR', 'Middleware')
54 | )
55 | )
56 | ->setIdentifier(
57 | (string)$this->io->ask(
58 | 'Enter an identifier for the middleware',
59 | $middleware->getIdentifierProposal($this->getProposalFromEnvironment('MIDDLEWARE_IDENTIFIER_PREFIX'))
60 | )
61 | )
62 | ->setType(
63 | (string)$this->io->choice(
64 | 'Choose the type (context) for the middleware',
65 | ['frontend', 'backend'],
66 | $this->getProposalFromEnvironment('MIDDLEWARE_TYPE', 'frontend')
67 | )
68 | )
69 | ->setBefore(
70 | GeneralUtility::trimExplode(
71 | ',',
72 | (string)$this->io->ask('Enter a comma separated list of identifiers the new middleware should be executed beforehand'),
73 | true
74 | )
75 | )
76 | ->setAfter(
77 | GeneralUtility::trimExplode(
78 | ',',
79 | (string)$this->io->ask('Enter a comma separated list of identifiers after which the new middleware should be executed'),
80 | true
81 | )
82 | );
83 | }
84 |
85 | /**
86 | * @param Middleware $component
87 | * @throws AbortCommandException
88 | */
89 | protected function publishComponentConfiguration(ComponentInterface $component): bool
90 | {
91 | $middlewareConfiguration = $this->arrayConfiguration->getConfiguration();
92 | if (isset($middlewareConfiguration[$component->getType()][$component->getIdentifier()])
93 | && !$this->io->confirm('The identifier ' . $component->getIdentifier() . ' already exists for type ' . $component->getType() . '. Do you want to override it?', true)
94 | ) {
95 | throw new AbortCommandException('Aborting middleware generation.', 1639664755);
96 | }
97 |
98 | $middlewareConfiguration[$component->getType()][$component->getIdentifier()] = $component->getArrayConfiguration();
99 | $this->arrayConfiguration->setConfiguration($middlewareConfiguration);
100 | if (!$this->writeArrayConfiguration()) {
101 | $this->io->error('Updating middleware configuration failed.');
102 | return false;
103 | }
104 |
105 | $this->io->success('Successfully created the middleware ' . $component->getName() . ' (' . $component->getIdentifier() . ').');
106 | return true;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Classes/Command/Component/SimpleComponentCommand.php:
--------------------------------------------------------------------------------
1 | addArgument('extensionKey', InputArgument::OPTIONAL);
57 | }
58 |
59 | /**
60 | * Initialization of context, e.g. extension key and package
61 | */
62 | protected function initialize(InputInterface $input, OutputInterface $output): void
63 | {
64 | parent::initialize($input, $output);
65 |
66 | $this->package = $this->getPackage($input);
67 | if ($this->package === null || !$this->package->getValueFromComposerManifest()) {
68 | throw new InvalidPackageException(
69 | 'The requested extension is invalid. You may want to execute "bin/typo3 make:extension".',
70 | 1639664756
71 | );
72 | }
73 | $this->extensionKey = $this->package->getPackageKey();
74 | $this->psr4Prefix = $this->getPsr4Prefix($this->package);
75 | }
76 |
77 | /**
78 | * Execute component generation. Extending classes MAY NOT override this method
79 | * but instead only provide necessary information via the abstract methods.
80 | */
81 | protected function execute(InputInterface $input, OutputInterface $output): int
82 | {
83 | $component = $this->createComponent();
84 | $absoluteComponentDirectory = $this->getAbsoluteComponentDirectory($component);
85 |
86 | if (!file_exists($absoluteComponentDirectory)) {
87 | try {
88 | GeneralUtility::mkdir_deep($absoluteComponentDirectory);
89 | } catch (\Exception $e) {
90 | $this->io->error('Creating of directory ' . $absoluteComponentDirectory . ' failed.');
91 | return 1;
92 | }
93 | }
94 |
95 | // Use .php in case no file extension was given
96 | $fileInfo = pathinfo($component->getName());
97 | $filename = $fileInfo['filename'] . '.' . (($fileInfo['extension'] ?? false) ? $fileInfo['extension'] : 'php');
98 |
99 | $componentFile = rtrim($absoluteComponentDirectory, '/') . '/' . $filename;
100 | if (file_exists($componentFile)
101 | && !$this->io->confirm('The file ' . $componentFile . ' already exists. Do you want to override it?')
102 | ) {
103 | $this->io->note('Aborting component generation.');
104 | return 0;
105 | }
106 |
107 | if (!GeneralUtility::writeFile($componentFile, (string)$component)) {
108 | $this->io->error('Creating ' . $component->getName() . ' in ' . $componentFile . ' failed.');
109 | return 1;
110 | }
111 |
112 | try {
113 | if (!$this->publishComponentConfiguration($component)) {
114 | return 1;
115 | }
116 | } catch (AbortCommandException $e) {
117 | $this->io->note($e->getMessage());
118 | return 0;
119 | }
120 |
121 | if ($this->showFlushCacheMessage) {
122 | $this->io->note('You might want to flush the cache now');
123 | }
124 |
125 | return 0;
126 | }
127 |
128 | protected function getPsr4Prefix(PackageInterface $package): string
129 | {
130 | return (string)key((array)($package->getValueFromComposerManifest('autoload')->{'psr-4'} ?? []));
131 | }
132 |
133 | protected function getExtensionClassesPath(PackageInterface $package, string $psr4Prefix): string
134 | {
135 | $classesPath = (string)($package->getValueFromComposerManifest('autoload')->{'psr-4'}->{$psr4Prefix} ?? '');
136 | return $classesPath ? (trim($classesPath, '/') . '/') : '';
137 | }
138 |
139 | /**
140 | * Initialize the service configuration for the current package
141 | */
142 | protected function initializeServiceConfiguration(): void
143 | {
144 | $this->serviceConfiguration = new ServiceConfiguration($this->package->getPackagePath());
145 |
146 | if (!isset($this->serviceConfiguration->getConfiguration()['services'])) {
147 | $basicConfiguration = (bool)$this->io->confirm('Your extension does not yet contain a service configuration. May we add one for you?', true);
148 | if (!$basicConfiguration) {
149 | throw new \RuntimeException('Can not add component without a service configuration.', 1639664757);
150 | }
151 | // Create basic service configuration for the extension
152 | $this->serviceConfiguration->createBasicServiceConfiguration($this->psr4Prefix);
153 | }
154 | }
155 |
156 | /**
157 | * Write the updated service configuration for the current package
158 | *
159 | * @throws AbortCommandException
160 | */
161 | public function writeServiceConfiguration(ServiceConfigurationComponentInterface $component): bool
162 | {
163 | $configuration = $this->serviceConfiguration->getConfiguration();
164 |
165 | if (!isset($configuration['services'])) {
166 | // Service configuration does not exist or was not properly initialized
167 | return false;
168 | }
169 |
170 | if (isset($configuration['services'][$component->getClassName()])
171 | && !$this->io->confirm('A service configuration for ' . $component->getClassName() . ' already exists. Do you want to override it?', true)
172 | ) {
173 | throw new AbortCommandException('Aborting component generation.', 1639664758);
174 | }
175 |
176 | $configuration['services'] = array_replace_recursive(
177 | $configuration['services'],
178 | $component->getServiceConfiguration()
179 | );
180 |
181 | return $this->serviceConfiguration->setConfiguration($configuration)->write();
182 | }
183 |
184 | /**
185 | * Initialize an array configuration for the current package
186 | */
187 | protected function initializeArrayConfiguration(string $file, string $directory = 'Configuration/'): void
188 | {
189 | $this->arrayConfiguration = new ArrayConfiguration($this->package->getPackagePath(), $file, $directory);
190 | if ($this->arrayConfiguration->getConfiguration() === []) {
191 | $this->io->note('The configuration file ' . $directory . $file . ' does not yet exist. It will be automatically created.');
192 | }
193 | }
194 |
195 | /**
196 | * Write the updated array configuration for the current package
197 | */
198 | protected function writeArrayConfiguration(): bool
199 | {
200 | if ($this->arrayConfiguration->getConfiguration() === []) {
201 | // Array configuration was not properly set
202 | return false;
203 | }
204 |
205 | return $this->arrayConfiguration->write();
206 | }
207 |
208 | /**
209 | * Returns the absolute path to the component directory, while assuming that all
210 | * components are in the extensions classes directory. Can be overwritten in commands,
211 | * if this is not the case.
212 | */
213 | protected function getAbsoluteComponentDirectory(ComponentInterface $component): string
214 | {
215 | return $this->package->getPackagePath()
216 | . $this->getExtensionClassesPath($this->package, $this->psr4Prefix)
217 | . $component->getDirectory();
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/Classes/Command/Component/TestingSetupCommand.php:
--------------------------------------------------------------------------------
1 | setDescription('Create a docker based testing environment setup');
39 | }
40 |
41 | protected function execute(InputInterface $input, OutputInterface $output): int
42 | {
43 | $this->showFlushCacheMessage = false;
44 |
45 | $this->file = 'runTests.sh';
46 | $this->folder = 'Build/Scripts/';
47 | parent::execute($input, $output);
48 |
49 | $this->file = 'docker-compose.yml';
50 | $this->folder = 'Build/testing-docker/';
51 | parent::execute($input, $output);
52 |
53 | $this->io->success(
54 | 'The docker based testing environment setup is ready. You can enter the root directory of ' .
55 | $this->package->getPackageKey() . ' and execute: "bash Build/Scripts/runTests.sh -h"'
56 | );
57 |
58 | $this->io->note('Running specific test suits like "cgl" or "unit" requires installing the corresponding packages and configuration.');
59 |
60 | return 0;
61 | }
62 |
63 | protected function createComponent(): ComponentInterface
64 | {
65 | return (new TestingSetup($this->psr4Prefix))
66 | ->setExtensionKey($this->extensionKey)
67 | ->setDirectory($this->folder)
68 | ->setName($this->file);
69 | }
70 |
71 | protected function publishComponentConfiguration(ComponentInterface $component): bool
72 | {
73 | // As we do not need to publish a configuration, we just return true
74 | return true;
75 | }
76 |
77 | protected function getAbsoluteComponentDirectory(ComponentInterface $component): string
78 | {
79 | return $this->package->getPackagePath() . $this->folder;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Classes/Command/ExtensionCommand.php:
--------------------------------------------------------------------------------
1 | setDescription('Create a TYPO3 extension');
30 | }
31 |
32 | protected function execute(InputInterface $input, OutputInterface $output): int
33 | {
34 | $packageName = (string)$this->io->ask(
35 | 'Enter the composer package name (e.g. "vendor/awesome")',
36 | null,
37 | [$this, 'validatePackageKey']
38 | );
39 |
40 | [,$packageKey] = explode('/', $packageName);
41 |
42 | $extensionKey = (string)$this->io->ask(
43 | 'Enter the extension key',
44 | str_replace('-', '_', $packageKey)
45 | );
46 |
47 | $psr4Prefix = (string)$this->io->ask(
48 | 'Enter the PSR-4 namespace',
49 | str_replace(['_', '-'], [], ucwords($packageName, '/-_'))
50 | );
51 |
52 | $availableTypo3Versions = [
53 | '^10.4' => 'TYPO3 v10 LTS',
54 | '^11.5' => 'TYPO3 v11 LTS',
55 | '^12.4' => 'TYPO3 v12 LTS',
56 | '^13.4' => 'TYPO3 v13 LTS',
57 | ];
58 | $question = $this->io->askQuestion((new ChoiceQuestion(
59 | 'Choose supported TYPO3 versions (comma separate for multiple)',
60 | array_combine([10, 11, 12, 13], array_values($availableTypo3Versions)),
61 | 12
62 | ))->setMultiselect(true));
63 |
64 | $supportedTypo3Versions = [];
65 | foreach ($question as $resultPosition) {
66 | $versionConstraint = array_search($resultPosition, $availableTypo3Versions, true);
67 | $supportedTypo3Versions[$this->getMajorVersion($versionConstraint)] = $versionConstraint;
68 | }
69 |
70 | $description = $this->io->ask(
71 | 'Enter a description of the extension',
72 | null,
73 | [$this, 'answerRequired']
74 | );
75 |
76 | $directory = (string)$this->io->ask(
77 | 'Where should the extension be created?',
78 | $this->getProposalFromEnvironment('EXTENSION_DIR', 'src/extensions/')
79 | );
80 |
81 | $extension = (new Extension())
82 | ->setPackageName($packageName)
83 | ->setPackageKey($packageKey)
84 | ->setExtensionKey($extensionKey)
85 | ->setPsr4Prefix($psr4Prefix)
86 | ->setTypo3Versions($supportedTypo3Versions)
87 | ->setDescription($description)
88 | ->setDirectory($directory);
89 |
90 | // Create extension directory
91 | $absoluteExtensionPath = $extension->getExtensionPath();
92 | if (!file_exists($absoluteExtensionPath)) {
93 | try {
94 | GeneralUtility::mkdir_deep($absoluteExtensionPath);
95 | } catch (\Exception $e) {
96 | $this->io->error('Creating of directory ' . $absoluteExtensionPath . ' failed');
97 | return 1;
98 | }
99 | }
100 |
101 | // Create composer.json
102 | $composerFile = rtrim($absoluteExtensionPath, '/') . '/composer.json';
103 | if (file_exists($composerFile)
104 | && !$this->io->confirm('A composer.json does already exist. Do you want to override it?', true)
105 | ) {
106 | $this->io->note('Creating composer.json skipped');
107 | } elseif (!GeneralUtility::writeFile($composerFile, json_encode($extension, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), true)) {
108 | $this->io->error('Creating composer.json failed');
109 | return 1;
110 | }
111 |
112 | // Add basic service configuration if requested
113 | if ($this->io->confirm('May we add a basic service configuration for you?', true)) {
114 | $serviceConfiguration = new ServiceConfiguration($absoluteExtensionPath);
115 | if ($serviceConfiguration->getConfiguration() !== []
116 | && !$this->io->confirm('A service configuration does already exist. Do you want to override it?', true)
117 | ) {
118 | $this->io->note('Creating service configuration skipped');
119 | } else {
120 | $serviceConfiguration->createBasicServiceConfiguration($extension->getPsr4Prefix());
121 | if (!$serviceConfiguration->write()) {
122 | $this->io->warning('Creating service configuration failed');
123 | return 1;
124 | }
125 | }
126 | }
127 |
128 | // Add ext_emconf.php if TYPO3 v10 or requested (default=NO)
129 | if (isset($supportedTypo3Versions[10])
130 | || $this->io->confirm('May we create a ext_emconf.php for you?', false)
131 | ) {
132 | $extEmConfFile = rtrim($absoluteExtensionPath, '/') . '/ext_emconf.php';
133 | if (file_exists($extEmConfFile)
134 | && !$this->io->confirm('A ext_emconf.php does already exist. Do you want to override it?')
135 | ) {
136 | $this->io->note('Creating ext_emconf.php skipped');
137 | } elseif (!GeneralUtility::writeFile($extEmConfFile, (string)$extension)) {
138 | $this->io->error('Creating ' . $extEmConfFile . ' failed.');
139 | return 1;
140 | }
141 | }
142 |
143 | // Create the "Classes/" folder
144 | if (!file_exists($absoluteExtensionPath . 'Classes/')) {
145 | try {
146 | GeneralUtility::mkdir($absoluteExtensionPath . 'Classes/');
147 | } catch (\Exception $e) {
148 | $this->io->error('Creating of the "Classes/" folder in ' . $absoluteExtensionPath . ' failed');
149 | return 1;
150 | }
151 | }
152 |
153 | $this->io->success('Successfully created the extension ' . $extension->getExtensionKey() . ' (' . $extension->getPackageName() . ').');
154 | $this->io->note('Depending on your installation, the extension now might have to be activated manually.');
155 |
156 | return 0;
157 | }
158 |
159 | protected function getMajorVersion(string $versionConstraint): int
160 | {
161 | return (int)preg_replace_callback(
162 | '/^\^([0-9]{1,2}).*$/',
163 | static function ($matches) { return $matches[1]; },
164 | $versionConstraint
165 | );
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/Classes/Component/AbstractComponent.php:
--------------------------------------------------------------------------------
1 | psr4Prefix = ucfirst(trim(str_replace('/', '\\', $psr4Prefix), '\\')) . '\\';
32 | }
33 |
34 | public function getName(): string
35 | {
36 | return $this->name;
37 | }
38 |
39 | public function setName(string $name): self
40 | {
41 | $this->name = ucfirst(str_replace(['/', '\\'], '', $name));
42 | return $this;
43 | }
44 |
45 | public function getDirectory(): string
46 | {
47 | return $this->directory;
48 | }
49 |
50 | public function setDirectory(string $directory): self
51 | {
52 | $this->directory = ltrim($directory, '/');
53 | return $this;
54 | }
55 |
56 | public function getClassName(): string
57 | {
58 | return $this->getNamespace() . '\\' . $this->name;
59 | }
60 |
61 | public function getIdentifierProposal(string $prefix = ''): string
62 | {
63 | $packagePrefix = $prefix ?: mb_strtolower(
64 | trim(
65 | preg_replace(
66 | '/(?<=\\w)([A-Z])/',
67 | '-\\1',
68 | trim(str_replace('\\', '/', $this->psr4Prefix), '/')
69 | ) ?? '',
70 | '-'
71 | ),
72 | 'utf-8'
73 | );
74 |
75 | $identifier = mb_strtolower(
76 | trim(preg_replace('/(?<=\\w)([A-Z])/', '-\\1', $this->name) ?? '', '-'),
77 | 'utf-8'
78 | );
79 |
80 | return $packagePrefix . '/' . $identifier;
81 | }
82 |
83 | protected function getNamespace(): string
84 | {
85 | return rtrim($this->psr4Prefix . ucfirst(trim(str_replace('/', '\\', $this->directory), '\\')), '\\');
86 | }
87 |
88 | protected function createFileContent(string $fileName, array $replace): string
89 | {
90 | return (string)preg_replace_callback(
91 | '/\{\{([A-Z_]*)\}\}/',
92 | static function ($result) use ($replace): string {
93 | return $replace[$result[1]] ?? $result[0];
94 | },
95 | file_get_contents(__DIR__ . '/../../Resources/Private/CodeTemplates/' . $fileName)
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Classes/Component/ArrayConfigurationComponentInterface.php:
--------------------------------------------------------------------------------
1 | routeIdentifier;
32 | }
33 |
34 | public function getRouteIdentifierProposal(string $prefix): string
35 | {
36 | return 'tx_' . trim($prefix, '_') . '_' . mb_strtolower(
37 | trim(str_replace('Controller', '', preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $this->name)), '_'),
38 | 'utf-8'
39 | );
40 | }
41 |
42 | public function setRouteIdentifier(string $routeIdentifier): BackendController
43 | {
44 | $this->routeIdentifier = trim(str_replace('-', '_', $routeIdentifier), '_');
45 | return $this;
46 | }
47 |
48 | public function getRoutePathProposal(): string
49 | {
50 | return mb_strtolower(
51 | '/' . trim(str_replace('_', '/', str_replace('tx_', '', $this->routeIdentifier)), '/)'),
52 | 'utf-8'
53 | );
54 | }
55 |
56 | public function setRoutePath(string $routePath): BackendController
57 | {
58 | $this->routePath = '/' . trim($routePath, '/');
59 | return $this;
60 | }
61 |
62 | public function setMethodName(string $methodName): BackendController
63 | {
64 | $this->methodName = $methodName;
65 | return $this;
66 | }
67 |
68 | public function getArrayConfiguration(): array
69 | {
70 | return [
71 | 'path' => $this->routePath,
72 | 'target' => $this->getClassName() . ($this->methodName !== '' ? '::' . $this->methodName : ''),
73 | ];
74 | }
75 |
76 | public function __toString(): string
77 | {
78 | return $this->createFileContent(
79 | 'BackendController.php',
80 | [
81 | 'NAMESPACE' => $this->getNamespace(),
82 | 'NAME' => $this->name,
83 | 'METHOD' => $this->methodName ?: '__invoke',
84 | ]
85 | );
86 | }
87 |
88 | public function getServiceConfiguration(): array
89 | {
90 | return [
91 | $this->getClassName() => [
92 | 'tags' => [
93 | 'backend.controller',
94 | ],
95 | ],
96 | ];
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Classes/Component/Command.php:
--------------------------------------------------------------------------------
1 | commandName;
32 | }
33 |
34 | public function getCommandNameProposal(string $extensionKey): string
35 | {
36 | $extensionPrefix = trim(str_replace('_', '-', $extensionKey), '-');
37 | $commandName = trim(
38 | str_replace(
39 | 'command',
40 | '',
41 | mb_strtolower(preg_replace('/(?<=\\w)([A-Z])/', '-\\1', $this->name) ?? '', 'utf-8')
42 | ),
43 | '-'
44 | );
45 |
46 | return $extensionPrefix . ':' . $commandName;
47 | }
48 |
49 | public function setCommandName(string $commandName): self
50 | {
51 | $this->commandName = $commandName;
52 | return $this;
53 | }
54 |
55 | public function setDescription(string $description): self
56 | {
57 | $this->description = $description;
58 | return $this;
59 | }
60 |
61 | public function setSchedulable(bool $schedulable): self
62 | {
63 | $this->schedulable = $schedulable;
64 | return $this;
65 | }
66 |
67 | public function __toString(): string
68 | {
69 | return $this->createFileContent(
70 | 'Command.php',
71 | [
72 | 'NAMESPACE' => $this->getNamespace(),
73 | 'NAME' => $this->name,
74 | 'DESCRIPTION' => str_replace('\'', '\\\'', $this->description),
75 | ]
76 | );
77 | }
78 |
79 | public function getServiceConfiguration(): array
80 | {
81 | return [
82 | $this->getClassName() => [
83 | 'tags' => [
84 | [
85 | 'name' => 'console.command',
86 | 'command' => $this->commandName,
87 | 'description' => $this->description,
88 | 'schedulable' => $this->schedulable,
89 | ],
90 | ],
91 | ],
92 | ];
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Classes/Component/ComponentInterface.php:
--------------------------------------------------------------------------------
1 | eventName, '\\'));
32 | return end($parts) . 'Listener';
33 | }
34 |
35 | public function setIdentifier(string $identifier): self
36 | {
37 | $this->identifier = $identifier;
38 | return $this;
39 | }
40 |
41 | public function getEventName(): string
42 | {
43 | return $this->eventName;
44 | }
45 |
46 | public function setEventName(string $eventName): self
47 | {
48 | $this->eventName = '\\' . ltrim(str_replace('/', '\\', $eventName), '\\');
49 | return $this;
50 | }
51 |
52 | public function setMethodName(string $methodName): self
53 | {
54 | $this->methodName = $methodName;
55 | return $this;
56 | }
57 |
58 | public function __toString(): string
59 | {
60 | return $this->createFileContent(
61 | 'EventListener.php',
62 | [
63 | 'NAMESPACE' => $this->getNamespace(),
64 | 'NAME' => $this->name,
65 | 'METHOD' => $this->methodName ?: '__invoke',
66 | 'EVENT' => $this->eventName,
67 | ]
68 | );
69 | }
70 |
71 | public function getServiceConfiguration(): array
72 | {
73 | $configuration = [
74 | $this->getClassName() => [
75 | 'tags' => [
76 | [
77 | 'name' => 'event.listener',
78 | 'identifier' => $this->identifier,
79 | 'event' => ltrim($this->eventName, '\\'),
80 | ],
81 | ],
82 | ],
83 | ];
84 | if ($this->methodName !== '') {
85 | $configuration[$this->getClassName()]['tags'][0]['method'] = $this->methodName;
86 | }
87 |
88 | return $configuration;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Classes/Component/Extension.php:
--------------------------------------------------------------------------------
1 | */
36 | protected $typo3Versions = [];
37 |
38 | /** @var string */
39 | protected $description = '';
40 |
41 | /** @var string */
42 | protected $directory = '';
43 |
44 | public function getPackageName(): string
45 | {
46 | return $this->packageName;
47 | }
48 |
49 | public function setPackageName(string $packageName): self
50 | {
51 | $this->packageName = $packageName;
52 | return $this;
53 | }
54 |
55 | public function setPackageKey(string $packageKey): self
56 | {
57 | $this->packageKey = $packageKey;
58 | return $this;
59 | }
60 |
61 | public function getExtensionKey(): string
62 | {
63 | return $this->extensionKey;
64 | }
65 |
66 | public function setExtensionKey(string $extensionKey): self
67 | {
68 | $this->extensionKey = $extensionKey;
69 | return $this;
70 | }
71 |
72 | public function getPsr4Prefix(): string
73 | {
74 | return $this->psr4Prefix;
75 | }
76 |
77 | public function setPsr4Prefix(string $psr4Prefix): self
78 | {
79 | $this->psr4Prefix = str_replace('/', '\\', $psr4Prefix) . '\\';
80 | return $this;
81 | }
82 |
83 | public function setTypo3Versions(array $typo3Versions): self
84 | {
85 | asort($typo3Versions);
86 | $this->typo3Versions = $typo3Versions;
87 | return $this;
88 | }
89 |
90 | public function setDescription(string $description): self
91 | {
92 | $this->description = $description;
93 | return $this;
94 | }
95 |
96 | public function setDirectory(string $directory): self
97 | {
98 | $this->directory = $directory;
99 | return $this;
100 | }
101 |
102 | public function getExtensionPath(): string
103 | {
104 | return Environment::getProjectPath() . '/' . trim($this->directory, '/') . '/' . $this->packageKey . '/';
105 | }
106 |
107 | public function jsonSerialize(): array
108 | {
109 | return $this->createComposerManifest();
110 | }
111 |
112 | public function __toString()
113 | {
114 | return "createExtensionConfiguration()) . ";\n";
115 | }
116 |
117 | protected function createComposerManifest(): array
118 | {
119 | return [
120 | 'name' => $this->packageName,
121 | 'description' => $this->description,
122 | 'type' => 'typo3-cms-extension',
123 | 'license' => ['GPL-2.0-or-later'],
124 | 'require' => [
125 | 'typo3/cms-core' => implode(' || ', $this->typo3Versions),
126 | ],
127 | 'autoload' => [
128 | 'psr-4' => [
129 | $this->psr4Prefix => 'Classes/',
130 | ],
131 | ],
132 | 'extra' => [
133 | 'typo3/cms' => [
134 | 'extension-key' => $this->extensionKey,
135 | ],
136 | ],
137 | ];
138 | }
139 |
140 | protected function createExtensionConfiguration(): array
141 | {
142 | return [
143 | 'title' => $this->extensionKey,
144 | 'description' => $this->description,
145 | 'constraints' => [
146 | 'depends' => [
147 | 'typo3' => $this->getTypo3Constraint(),
148 | ],
149 | ],
150 | 'autoload' => [
151 | 'psr-4' => [
152 | str_replace('\\', '\\\\', $this->psr4Prefix) => 'Classes/',
153 | ],
154 | ],
155 | ];
156 | }
157 |
158 | protected function getTypo3Constraint(): string
159 | {
160 | $min = $this->typo3Versions[array_key_first($this->typo3Versions)];
161 | $max = $this->typo3Versions[array_key_last($this->typo3Versions)];
162 |
163 | // While ^13.0 will also include upcoming sprint releases,
164 | // we need to set minor version "4" for the max version.
165 | // @todo Should be handled more efficient
166 | if ($max === '^13.0') {
167 | $max = str_replace('.0', '.4', $max);
168 | }
169 |
170 | return sprintf(
171 | '%s-%s',
172 | str_replace('^', '', $min) . '.0',
173 | str_replace('^', '', $max) . '.99'
174 | );
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/Classes/Component/Middleware.php:
--------------------------------------------------------------------------------
1 | identifier;
35 | }
36 |
37 | public function setIdentifier(string $identifier): self
38 | {
39 | $this->identifier = $identifier;
40 | return $this;
41 | }
42 |
43 | public function getType(): string
44 | {
45 | return $this->type;
46 | }
47 |
48 | public function setType(string $type): self
49 | {
50 | $this->type = $type;
51 | return $this;
52 | }
53 |
54 | public function setBefore(array $before): self
55 | {
56 | $this->before = $before;
57 | return $this;
58 | }
59 |
60 | public function setAfter(array $after): self
61 | {
62 | $this->after = $after;
63 | return $this;
64 | }
65 |
66 | public function __toString(): string
67 | {
68 | return $this->createFileContent(
69 | 'Middleware.php',
70 | [
71 | 'NAMESPACE' => $this->getNamespace(),
72 | 'NAME' => $this->name,
73 | ]
74 | );
75 | }
76 |
77 | public function getArrayConfiguration(): array
78 | {
79 | $configuration = [
80 | 'target' => $this->getClassName(),
81 | ];
82 |
83 | if ($this->before !== []) {
84 | $configuration['before'] = $this->before;
85 | }
86 |
87 | if ($this->after !== []) {
88 | $configuration['after'] = $this->after;
89 | }
90 |
91 | return $configuration;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Classes/Component/ServiceConfigurationComponentInterface.php:
--------------------------------------------------------------------------------
1 | extensionKey;
26 | }
27 |
28 | public function setExtensionKey(string $extensionKey): self
29 | {
30 | $this->extensionKey = $extensionKey;
31 | return $this;
32 | }
33 |
34 | public function setName(string $name): AbstractComponent
35 | {
36 | $this->name = $name;
37 | return $this;
38 | }
39 |
40 | public function __toString(): string
41 | {
42 | return $this->createFileContent(
43 | $this->getDirectory() . $this->getName(),
44 | [
45 | 'EXTENSION_KEY' => $this->getExtensionKey(),
46 | ]
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Classes/Environment/Variables.php:
--------------------------------------------------------------------------------
1 | packagePath = rtrim($packagePath, '/') . '/';
29 | $this->configuration = $this->load();
30 | }
31 |
32 | abstract protected function load(): array;
33 |
34 | public function getConfiguration(): array
35 | {
36 | return $this->configuration;
37 | }
38 |
39 | public function setConfiguration(array $configuration): ConfigurationInterface
40 | {
41 | $this->configuration = $configuration;
42 | return $this;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Classes/IO/ArrayConfiguration.php:
--------------------------------------------------------------------------------
1 | file = trim($file, '/');
32 | $this->directory = trim($directory, '/') . '/';
33 | parent::__construct($packagePath);
34 | }
35 |
36 | /**
37 | * Write / update the array configuration
38 | *
39 | * @return bool Whether the array configuration was updated successfully
40 | */
41 | public function write(): bool
42 | {
43 | $directory = $this->packagePath . $this->directory;
44 | if (!file_exists($directory)) {
45 | GeneralUtility::mkdir_deep($directory);
46 | }
47 | $file = $directory . $this->file;
48 | return GeneralUtility::writeFile($file, "configuration) . ";\n", true);
49 | }
50 |
51 | /**
52 | * Load the array configuration
53 | */
54 | protected function load(): array
55 | {
56 | $configurationFile = $this->packagePath . $this->directory . $this->file;
57 | if (!file_exists($configurationFile)) {
58 | return [];
59 | }
60 | $configuration = require $configurationFile;
61 |
62 | return is_array($configuration) ? $configuration : [];
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Classes/IO/ConfigurationInterface.php:
--------------------------------------------------------------------------------
1 | packagePath . self::CONFIGURATION_DIRECTORY;
34 | if (!file_exists($directory)) {
35 | GeneralUtility::mkdir_deep($directory);
36 | }
37 | $file = $directory . self::CONFIGURATION_FILE;
38 | return GeneralUtility::writeFile($file, Yaml::dump($this->sortImportsOnTop($this->configuration), 99, 2), true);
39 | }
40 |
41 | /**
42 | * Initialize a new basic service configuration
43 | */
44 | public function createBasicServiceConfiguration(string $psr4Prefix): void
45 | {
46 | $this->configuration['services'] = [
47 | '_defaults' => [
48 | 'autowire' => true,
49 | 'autoconfigure' => true,
50 | 'public' => false,
51 | ],
52 | trim(str_replace('/', '\\', ucfirst($psr4Prefix)), '\\') . '\\' => [
53 | 'resource' => '../Classes/*',
54 | 'exclude' => '../Classes/Domain/Model/*',
55 | ],
56 | ];
57 | }
58 |
59 | /**
60 | * Load the service configuration
61 | */
62 | protected function load(): array
63 | {
64 | $serviceConfiguration = $this->packagePath . self::CONFIGURATION_DIRECTORY . self::CONFIGURATION_FILE;
65 |
66 | if (!file_exists($serviceConfiguration)) {
67 | return [];
68 | }
69 |
70 | try {
71 | $configuration = Yaml::parse(file_get_contents($serviceConfiguration) ?: '');
72 | } catch (\Exception $e) {
73 | // In case configuration can not be loaded / parsed return an empty array
74 | return [];
75 | }
76 |
77 | return is_array($configuration) ? $configuration : [];
78 | }
79 |
80 | protected function sortImportsOnTop(array $newConfiguration): array
81 | {
82 | ksort($newConfiguration);
83 | if (isset($newConfiguration['imports'])) {
84 | $imports = $newConfiguration['imports'];
85 | unset($newConfiguration['imports']);
86 | $newConfiguration = array_merge(['imports' => $imports], $newConfiguration);
87 | }
88 | return $newConfiguration;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Classes/PackageResolver.php:
--------------------------------------------------------------------------------
1 | packageManager = $packageManager;
30 | }
31 |
32 | public function resolvePackage(string $extensionKey): ?PackageInterface
33 | {
34 | try {
35 | return $this->packageManager->getPackage($extensionKey);
36 | } catch (UnknownPackageException $e) {
37 | return null;
38 | }
39 | }
40 |
41 | /**
42 | * @return PackageInterface[]
43 | */
44 | public function getAvailablePackages(): array
45 | {
46 | return $this->packageManager->getAvailablePackages();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Configuration/Services.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | _defaults:
3 | autowire: true
4 | autoconfigure: true
5 | public: false
6 |
7 | B13\Make\:
8 | resource: '../Classes/*'
9 |
10 | B13\Make\PackageResolver:
11 | public: true
12 |
13 | B13\Make\Command\Component\BackendControllerCommand:
14 | tags:
15 | - name: 'console.command'
16 | command: 'make:backendcontroller'
17 | description: 'Create a backend controller'
18 | schedulable: false
19 |
20 | B13\Make\Command\Component\CommandCommand:
21 | tags:
22 | - name: 'console.command'
23 | command: 'make:command'
24 | description: 'Create a console command'
25 | schedulable: false
26 |
27 | B13\Make\Command\Component\EventListenerCommand:
28 | tags:
29 | - name: 'console.command'
30 | command: 'make:eventlistener'
31 | description: 'Create a PSR-14 event listener'
32 | schedulable: false
33 |
34 | B13\Make\Command\ExtensionCommand:
35 | tags:
36 | - name: 'console.command'
37 | command: 'make:extension'
38 | description: 'Create a TYPO3 extension'
39 | schedulable: false
40 |
41 | B13\Make\Command\Component\MiddlewareCommand:
42 | tags:
43 | - name: 'console.command'
44 | command: 'make:middleware'
45 | description: 'Create a PSR-15 middleware'
46 | schedulable: false
47 |
48 | B13\Make\Command\Component\TestingSetupCommand:
49 | tags:
50 | - name: 'console.command'
51 | command: 'make:testing:setup'
52 | description: 'Create a docker based testing environment setup'
53 | schedulable: false
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Make - A TYPO3 extension to kickstart extensions and components
2 |
3 | This TYPO3 extension allows to easily kickstart new TYPO3 extensions
4 | and components, such as Middlewares, Commands or Event listeners, by
5 | using an intuitive CLI approach.
6 |
7 | TYPO3 Explained offers an extended tutorial on how to
8 | [Kickstart a TYPO3 Extension with Make](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ExtensionArchitecture/Tutorials/Kickstart/Make/Index.html).
9 |
10 | ## Installation
11 |
12 | Install this extension as "dev" dependency via `composer req b13/make --dev`.
13 |
14 | You can also download the extension from the
15 | [TYPO3 Extension Repository](https://extensions.typo3.org/extension/make/)
16 | and activate it in the Extension Manager of your TYPO3 installation.
17 |
18 | Note: This extension is compatible with TYPO3 v10, v11, v12 and v13 and should
19 | only be used in development context. So please make sure it is excluded
20 | for production releases.
21 |
22 | ## Usage
23 |
24 | All components, including new extensions, can be created with
25 | a dedicated command, executed on CLI with the ```typo3``` binary:
26 | `bin/typo3 make:`.
27 |
28 | Example for creating a new extension:
29 |
30 | ```bash
31 | bin/typo3 make:extension
32 | ```
33 |
34 | All commands are interactive, which means you have to configure the
35 | extension or component by answering the displayed questions. Most of
36 | them automatically suggest a best practice default value, e.g. for
37 | identifiers or namespaces, which can just be confirmed.
38 |
39 | It's also possible to customize those default values using environment
40 | variables with the `B13_MAKE_` prefix. The full list is shown below:
41 |
42 | - `B13_MAKE_BACKEND_CONTROLLER_DIR` - Default directory for backend controllers
43 | - `B13_MAKE_BACKEND_CONTROLLER_PREFIX` - Default prefix for the backend controllers' route identifier
44 | - `B13_MAKE_COMMAND_DIR` - Default directory for commands
45 | - `B13_MAKE_COMMAND_NAME_PREFIX` - Default prefix for commands
46 | - `B13_MAKE_EVENT_LISTENER_DIR` - Default directory for event listeners
47 | - `B13_MAKE_EVENT_LISTENER_IDENTIFIER_PREFIX` - Default identifier prefix for event listeners
48 | - `B13_MAKE_EXTENSION_DIR` - Default directory for extensions
49 | - `B13_MAKE_MIDDLEWARE_DIR` - Default directory for middlewares
50 | - `B13_MAKE_MIDDLEWARE_IDENTIFIER_PREFIX` - Default identifier prefix for middlewares
51 | - `B13_MAKE_MIDDLEWARE_TYPE` - Default context type for middlewares
52 |
53 | All component related commands require an extension name, for which the
54 | component should be created. This can also be set as first argument or
55 | globally with the `B13_MAKE_EXTENSION_KEY` environment variable.
56 |
57 | ## Commands
58 |
59 | Following commands are available
60 |
61 | - `make:backendcontroller` - Create a new backend controller
62 | - `make:command` - Create a new command
63 | - `make:eventlistener` - Create a new event listener
64 | - `make:extension` - Create a new extension
65 | - `make:middleware` - Create a new middleware
66 |
67 | ## Credits
68 |
69 | This extension was created by Oliver Bartsch in 2021 for [b13 GmbH, Stuttgart](https://b13.com).
70 |
71 | [Find more TYPO3 extensions we have developed](https://b13.com/useful-typo3-extensions-from-b13-to-you)
72 | that help us deliver value in client projects. As part of the way we work,
73 | we focus on testing and best practices ensuring long-term performance,
74 | reliability, and results in all our code.
75 |
--------------------------------------------------------------------------------
/Resources/Private/CodeTemplates/BackendController.php:
--------------------------------------------------------------------------------
1 | responseFactory = $responseFactory;
23 | $this->streamFactory = $streamFactory;
24 | }
25 |
26 | public function {{METHOD}}(ServerRequestInterface $request): ResponseInterface
27 | {
28 | // Do awesome stuff
29 |
30 | return $this->responseFactory->createResponse()->withBody(
31 | $this->streamFactory->createStream('Response content from {{NAME}} with route path: ' . $request->getAttribute('route')->getPath())
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Resources/Private/CodeTemplates/Build/Scripts/runTests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | #
4 | # TYPO3 core test runner based on docker and docker-compose.
5 | #
6 |
7 | # Function to write a .env file in Build/testing-docker
8 | # This is read by docker-compose and vars defined here are
9 | # used in Build/testing-docker/docker-compose.yml
10 | setUpDockerComposeDotEnv() {
11 | # Delete possibly existing local .env file if exists
12 | [ -e .env ] && rm .env
13 | # Set up a new .env file for docker-compose
14 | {
15 | echo "COMPOSE_PROJECT_NAME=local"
16 | # To prevent access rights of files created by the testing, the docker image later
17 | # runs with the same user that is currently executing the script. docker-compose can't
18 | # use $UID directly itself since it is a shell variable and not an env variable, so
19 | # we have to set it explicitly here.
20 | echo "HOST_UID=`id -u`"
21 | # Your local home directory for composer and npm caching
22 | echo "HOST_HOME=${HOME}"
23 | echo "CORE_ROOT=${CORE_ROOT}"
24 | # Your local user
25 | echo "HOST_USER=${USER}"
26 | echo "TEST_FILE=${TEST_FILE}"
27 | echo "PHP_XDEBUG_ON=${PHP_XDEBUG_ON}"
28 | echo "PHP_XDEBUG_PORT=${PHP_XDEBUG_PORT}"
29 | echo "DOCKER_PHP_IMAGE=${DOCKER_PHP_IMAGE}"
30 | echo "TYPO3=${TYPO3}"
31 | echo "PHP_VERSION=${PHP_VERSION}"
32 | echo "EXTRA_TEST_OPTIONS=${EXTRA_TEST_OPTIONS}"
33 | echo "SCRIPT_VERBOSE=${SCRIPT_VERBOSE}"
34 | echo "CGLCHECK_DRY_RUN=${CGLCHECK_DRY_RUN}"
35 | echo "DOCKER_SELENIUM_IMAGE=${DOCKER_SELENIUM_IMAGE}"
36 | echo "IS_CORE_CI=${IS_CORE_CI}"
37 | echo "PHPSTAN_CONFIG_FILE=${PHPSTAN_CONFIG_FILE}"
38 | } > .env
39 | }
40 |
41 | # Load help text into $HELP
42 | read -r -d '' HELP <
54 | Specifies which test suite to run
55 | - acceptance: backend acceptance tests
56 | - cgl: cgl test and fix all php files
57 | - composerUpdate: "composer update", handy if host has no PHP
58 | - composerValidate: "composer validate"
59 | - functional: functional tests
60 | - lint: PHP linting
61 | - phpstan: phpstan analyze
62 | - unit (default): PHP unit tests
63 |
64 | -t <10|11>
65 | Only with -s composerUpdate|acceptance|functional
66 | TYPO3 core major version the extension is embedded in for testing.
67 |
68 | -d
69 | Only with -s functional
70 | Specifies on which DBMS tests are performed
71 | - mariadb (default): use mariadb
72 | - postgres: use postgres
73 | - sqlite: use sqlite
74 |
75 | -p <7.2|7.3|7.4|8.0|8.1>
76 | Specifies the PHP minor version to be used
77 | - 7.4 (default): use PHP 7.4
78 |
79 | -e ""
80 | Only with -s acceptance|functional|unit
81 | Additional options to send to phpunit (unit & functional tests) or codeception (acceptance
82 | tests). For phpunit, options starting with "--" must be added after options starting with "-".
83 | Example -e "-v --filter canRetrieveValueWithGP" to enable verbose output AND filter tests
84 | named "canRetrieveValueWithGP"
85 |
86 | -x
87 | Only with -s functional|unit|acceptance
88 | Send information to host instance for test or system under test break points. This is especially
89 | useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port
90 | can be selected with -y
91 |
92 | -y
93 | Send xdebug information to a different port than default 9003 if an IDE like PhpStorm
94 | is not listening on default port.
95 |
96 | -n
97 | Only with -s cgl
98 | Activate dry-run in CGL check that does not actively change files and only prints broken ones.
99 |
100 | -u
101 | Update existing typo3/core-testing-*:latest docker images. Maintenance call to docker pull latest
102 | versions of the main php images. The images are updated once in a while and only the youngest
103 | ones are supported by core testing. Use this if weird test errors occur. Also removes obsolete
104 | image versions of typo3/core-testing-*.
105 |
106 | -v
107 | Enable verbose script output. Shows variables and docker commands.
108 |
109 | -h
110 | Show this help.
111 |
112 | Examples:
113 | # Run unit tests using PHP 7.4
114 | ./Build/Scripts/runTests.sh
115 |
116 | # Run unit tests using PHP 7.3
117 | ./Build/Scripts/runTests.sh -p 7.3
118 | EOF
119 |
120 | # Test if docker-compose exists, else exit out with error
121 | if ! type "docker-compose" > /dev/null; then
122 | echo "This script relies on docker and docker-compose. Please install" >&2
123 | exit 1
124 | fi
125 |
126 | # docker-compose v2 is enabled by docker for mac as experimental feature without
127 | # asking the user. v2 is currently broken. Detect the version and error out.
128 | DOCKER_COMPOSE_VERSION=$(docker-compose version --short)
129 | DOCKER_COMPOSE_MAJOR=$(echo "$DOCKER_COMPOSE_VERSION" | cut -d'.' -f1 | tr -d 'v')
130 | if [ "$DOCKER_COMPOSE_MAJOR" -gt "1" ]; then
131 | echo "docker-compose $DOCKER_COMPOSE_VERSION is currently broken and not supported by runTests.sh."
132 | echo "If you are running Docker Desktop for MacOS/Windows disable 'Use Docker Compose V2 release candidate' (Settings > Experimental Features)"
133 | exit 1
134 | fi
135 |
136 | # Go to the directory this script is located, so everything else is relative
137 | # to this dir, no matter from where this script is called.
138 | THIS_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
139 | cd "$THIS_SCRIPT_DIR" || exit 1
140 |
141 | # Go to directory that contains the local docker-compose.yml file
142 | cd ../testing-docker || exit 1
143 |
144 | # Set core root path by checking whether realpath exists
145 | if ! command -v realpath &> /dev/null; then
146 | echo "Consider installing realpath for properly resolving symlinks" >&2
147 | CORE_ROOT="${PWD}/../../"
148 | else
149 | CORE_ROOT=$(realpath "${PWD}/../../")
150 | fi
151 |
152 | # Option defaults
153 | TEST_SUITE="unit"
154 | DBMS="mariadb"
155 | PHP_VERSION="7.4"
156 | PHP_XDEBUG_ON=0
157 | PHP_XDEBUG_PORT=9003
158 | EXTRA_TEST_OPTIONS=""
159 | SCRIPT_VERBOSE=0
160 | CGLCHECK_DRY_RUN=""
161 | TYPO3="10"
162 | DOCKER_SELENIUM_IMAGE="selenium/standalone-chrome:4.0.0-20211102"
163 | IS_CORE_CI=0
164 | PHPSTAN_CONFIG_FILE="phpstan.local.neon"
165 |
166 | # ENV var "CI" is set by gitlab-ci. We use it here to distinct 'local' and 'CI' environment.
167 | if [ "$CI" == "true" ]; then
168 | IS_CORE_CI=1
169 | PHPSTAN_CONFIG_FILE="phpstan.ci.neon"
170 | fi
171 |
172 | # Detect arm64 and use a seleniarm image.
173 | # In a perfect world selenium would have a arm64 integrated, but that is not on the horizon.
174 | # So for the time being we have to use seleniarm image.
175 | ARCH=$(uname -m)
176 | if [ $ARCH = "arm64" ]; then
177 | DOCKER_SELENIUM_IMAGE="seleniarm/standalone-chromium:4.1.2-20220227"
178 | echo "Architecture" $ARCH "requires" $DOCKER_SELENIUM_IMAGE "to run acceptance tests."
179 | fi
180 |
181 | # Option parsing
182 | # Reset in case getopts has been used previously in the shell
183 | OPTIND=1
184 | # Array for invalid options
185 | INVALID_OPTIONS=();
186 | # Simple option parsing based on getopts (! not getopt)
187 | while getopts ":s:d:p:e:t:xy:nhuv" OPT; do
188 | case ${OPT} in
189 | s)
190 | TEST_SUITE=${OPTARG}
191 | ;;
192 | d)
193 | DBMS=${OPTARG}
194 | ;;
195 | p)
196 | PHP_VERSION=${OPTARG}
197 | if ! [[ ${PHP_VERSION} =~ ^(7.2|7.3|7.4|8.0|8.1)$ ]]; then
198 | INVALID_OPTIONS+=("${OPTARG}")
199 | fi
200 | ;;
201 | t)
202 | TYPO3=${OPTARG}
203 | ;;
204 | e)
205 | EXTRA_TEST_OPTIONS=${OPTARG}
206 | ;;
207 | x)
208 | PHP_XDEBUG_ON=1
209 | ;;
210 | y)
211 | PHP_XDEBUG_PORT=${OPTARG}
212 | ;;
213 | h)
214 | echo "${HELP}"
215 | exit 0
216 | ;;
217 | n)
218 | CGLCHECK_DRY_RUN="-n"
219 | ;;
220 | u)
221 | TEST_SUITE=update
222 | ;;
223 | v)
224 | SCRIPT_VERBOSE=1
225 | ;;
226 | \?)
227 | INVALID_OPTIONS+=("${OPTARG}")
228 | ;;
229 | :)
230 | INVALID_OPTIONS+=("${OPTARG}")
231 | ;;
232 | esac
233 | done
234 |
235 | # Exit on invalid options
236 | if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then
237 | echo "Invalid option(s):" >&2
238 | for I in "${INVALID_OPTIONS[@]}"; do
239 | echo "-"${I} >&2
240 | done
241 | echo >&2
242 | echo "${HELP}" >&2
243 | exit 1
244 | fi
245 |
246 | # Move "7.4" to "php74", the latter is the docker container name
247 | DOCKER_PHP_IMAGE=$(echo "php${PHP_VERSION}" | sed -e 's/\.//')
248 |
249 | # Set $1 to first mass argument, this is the optional test file or test directory to execute
250 | shift $((OPTIND - 1))
251 | TEST_FILE=${1}
252 | if [ -n "${1}" ]; then
253 | TEST_FILE="Web/typo3conf/ext/{{EXTENSION_KEY}}/${1}"
254 | fi
255 |
256 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
257 | set -x
258 | fi
259 |
260 | # Suite execution
261 | case ${TEST_SUITE} in
262 | acceptance)
263 | setUpDockerComposeDotEnv
264 | docker-compose run acceptance_backend_mariadb10
265 | SUITE_EXIT_CODE=$?
266 | docker-compose down
267 | ;;
268 | cgl)
269 | # Active dry-run for cgl needs not "-n" but specific options
270 | if [ -n "${CGLCHECK_DRY_RUN}" ]; then
271 | CGLCHECK_DRY_RUN="--dry-run --diff"
272 | fi
273 | setUpDockerComposeDotEnv
274 | docker-compose run cgl
275 | SUITE_EXIT_CODE=$?
276 | docker-compose down
277 | ;;
278 | composerUpdate)
279 | setUpDockerComposeDotEnv
280 | docker-compose run composer_update
281 | SUITE_EXIT_CODE=$?
282 | docker-compose down
283 | ;;
284 | composerValidate)
285 | setUpDockerComposeDotEnv
286 | docker-compose run composer_validate
287 | SUITE_EXIT_CODE=$?
288 | docker-compose down
289 | ;;
290 | functional)
291 | setUpDockerComposeDotEnv
292 | case ${DBMS} in
293 | mariadb)
294 | docker-compose run functional_mariadb10
295 | SUITE_EXIT_CODE=$?
296 | ;;
297 | postgres)
298 | docker-compose run functional_postgres10
299 | SUITE_EXIT_CODE=$?
300 | ;;
301 | sqlite)
302 | mkdir -p ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/
303 | docker-compose run functional_sqlite
304 | SUITE_EXIT_CODE=$?
305 | ;;
306 | *)
307 | echo "Invalid -d option argument ${DBMS}" >&2
308 | echo >&2
309 | echo "${HELP}" >&2
310 | exit 1
311 | esac
312 | docker-compose down
313 | ;;
314 | lint)
315 | setUpDockerComposeDotEnv
316 | docker-compose run lint
317 | SUITE_EXIT_CODE=$?
318 | docker-compose down
319 | ;;
320 | phpstan)
321 | setUpDockerComposeDotEnv
322 | docker-compose run phpstan
323 | SUITE_EXIT_CODE=$?
324 | docker-compose down
325 | ;;
326 | unit)
327 | setUpDockerComposeDotEnv
328 | docker-compose run unit
329 | SUITE_EXIT_CODE=$?
330 | docker-compose down
331 | ;;
332 | update)
333 | # prune unused, dangling local volumes
334 | echo "> prune unused, dangling local volumes"
335 | docker volume ls -q -f driver=local -f dangling=true | awk '$0 ~ /^[0-9a-f]{64}$/ { print }' | xargs -I {} docker volume rm {}
336 | echo ""
337 | # pull typo3/core-testing-*:latest versions of those ones that exist locally
338 | echo "> pull typo3/core-testing-*:latest versions of those ones that exist locally"
339 | docker images typo3/core-testing-*:latest --format "{{.Repository}}:latest" | xargs -I {} docker pull {}
340 | echo ""
341 | # remove "dangling" typo3/core-testing-* images (those tagged as )
342 | echo "> remove \"dangling\" typo3/core-testing-* images (those tagged as )"
343 | docker images typo3/core-testing-* --filter "dangling=true" --format "{{.ID}}" | xargs -I {} docker rmi {}
344 | echo ""
345 | ;;
346 | *)
347 | echo "Invalid -s option argument ${TEST_SUITE}" >&2
348 | echo >&2
349 | echo "${HELP}" >&2
350 | exit 1
351 | esac
352 |
353 | echo "###########################################################################" >&2
354 | echo "Result of ${TEST_SUITE}" >&2
355 | if [[ ${IS_CORE_CI} -eq 1 ]]; then
356 | echo "Environment: CI" >&2
357 | else
358 | echo "Environment: local" >&2
359 | fi
360 | echo "PHP: ${PHP_VERSION}" >&2
361 |
362 | exit $SUITE_EXIT_CODE
363 |
--------------------------------------------------------------------------------
/Resources/Private/CodeTemplates/Build/testing-docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.3'
2 | services:
3 | chrome:
4 | image: ${DOCKER_SELENIUM_IMAGE}
5 | tmpfs:
6 | - /dev/shm:rw,nosuid,nodev,noexec,relatime
7 |
8 | mariadb10:
9 | # not using mariadb:10 for the time being, because 10.5.7 (currently latest) is broken
10 | image: mariadb:10.5.6
11 | environment:
12 | MYSQL_ROOT_PASSWORD: funcp
13 | tmpfs:
14 | - /var/lib/mysql/:rw,noexec,nosuid
15 |
16 | postgres10:
17 | image: postgres:10-alpine
18 | environment:
19 | POSTGRES_PASSWORD: funcp
20 | POSTGRES_USER: ${HOST_USER}
21 | tmpfs:
22 | - /var/lib/postgresql/data:rw,noexec,nosuid
23 |
24 | web:
25 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
26 | user: "${HOST_UID}"
27 | stop_grace_period: 1s
28 | volumes:
29 | - ${CORE_ROOT}:${CORE_ROOT}
30 | - /etc/passwd:/etc/passwd:ro
31 | - /etc/group:/etc/group:ro
32 | environment:
33 | TYPO3_PATH_ROOT: ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/acceptance
34 | TYPO3_PATH_APP: ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/acceptance
35 | command: >
36 | /bin/sh -c "
37 | if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
38 | XDEBUG_MODE=\"off\" \
39 | php -S web:8000 -t ${CORE_ROOT}/.Build/Web
40 | else
41 | DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
42 | XDEBUG_MODE=\"debug,develop\" \
43 | XDEBUG_TRIGGER=\"foo\" \
44 | XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \
45 | php -S web:8000 -t ${CORE_ROOT}/.Build/Web
46 | fi
47 | "
48 |
49 | acceptance_backend_mariadb10:
50 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
51 | user: "${HOST_UID}"
52 | links:
53 | - mariadb10
54 | - chrome
55 | - web
56 | environment:
57 | typo3DatabaseName: func_test
58 | typo3DatabaseUsername: root
59 | typo3DatabasePassword: funcp
60 | typo3DatabaseHost: mariadb10
61 | volumes:
62 | - ${CORE_ROOT}:${CORE_ROOT}
63 | - ${HOST_HOME}:${HOST_HOME}
64 | - /etc/passwd:/etc/passwd:ro
65 | - /etc/group:/etc/group:ro
66 | working_dir: ${CORE_ROOT}/.Build
67 | command: >
68 | /bin/sh -c "
69 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
70 | set -x
71 | fi
72 | echo Waiting for database start...;
73 | while ! nc -z mariadb10 3306; do
74 | sleep 1;
75 | done;
76 | echo Database is up;
77 | php -v | grep '^PHP';
78 | mkdir -p Web/typo3temp/var/tests/
79 | COMMAND=\"vendor/codeception/codeception/codecept run Application -d -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Tests/codeception.yml ${TEST_FILE}\"
80 | if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
81 | XDEBUG_MODE=\"off\" \
82 | $${COMMAND};
83 | else
84 | DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
85 | XDEBUG_MODE=\"debug,develop\" \
86 | XDEBUG_TRIGGER=\"foo\" \
87 | XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \
88 | $${COMMAND};
89 | fi
90 | "
91 |
92 | cgl:
93 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
94 | user: "${HOST_UID}"
95 | volumes:
96 | - ${CORE_ROOT}:${CORE_ROOT}
97 | - ${HOST_HOME}:${HOST_HOME}
98 | - /etc/passwd:/etc/passwd:ro
99 | - /etc/group:/etc/group:ro
100 | working_dir: ${CORE_ROOT}
101 | command: >
102 | /bin/sh -c "
103 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
104 | set -x
105 | fi
106 | php -v | grep '^PHP';
107 | if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
108 | php -dxdebug.mode=off \
109 | .Build/bin/php-cs-fixer fix \
110 | -v \
111 | ${CGLCHECK_DRY_RUN} \
112 | --config=Build/php-cs-fixer.php \
113 | --using-cache=no .
114 | else
115 | DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
116 | XDEBUG_MODE=\"debug,develop\" \
117 | XDEBUG_TRIGGER=\"foo\" \
118 | XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \
119 | PHP_CS_FIXER_ALLOW_XDEBUG=1 \
120 | .Build/bin/php-cs-fixer fix \
121 | -v \
122 | ${CGLCHECK_DRY_RUN} \
123 | --config=Build/php-cs-fixer.php \
124 | --using-cache=no .
125 | fi
126 | "
127 |
128 | composer_update:
129 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
130 | user: "${HOST_UID}"
131 | volumes:
132 | - ${CORE_ROOT}:${CORE_ROOT}
133 | - ${HOST_HOME}:${HOST_HOME}
134 | - /etc/passwd:/etc/passwd:ro
135 | - /etc/group:/etc/group:ro
136 | working_dir: ${CORE_ROOT}
137 | command: >
138 | /bin/sh -c "
139 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
140 | set -x
141 | fi
142 | php -v | grep '^PHP';
143 | COMPOSER_HOME=${CORE_ROOT}/.Build/.composer composer update --no-progress --no-interaction;
144 | "
145 |
146 | composer_validate:
147 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
148 | user: "${HOST_UID}"
149 | volumes:
150 | - ${CORE_ROOT}:${CORE_ROOT}
151 | - ${HOST_HOME}:${HOST_HOME}
152 | - /etc/passwd:/etc/passwd:ro
153 | - /etc/group:/etc/group:ro
154 | working_dir: ${CORE_ROOT}
155 | command: >
156 | /bin/sh -c "
157 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
158 | set -x
159 | fi
160 | php -v | grep '^PHP';
161 | composer validate;
162 | "
163 |
164 | functional_mariadb10:
165 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
166 | user: "${HOST_UID}"
167 | links:
168 | - mariadb10
169 | volumes:
170 | - ${CORE_ROOT}:${CORE_ROOT}
171 | - ${HOST_HOME}:${HOST_HOME}
172 | - /etc/passwd:/etc/passwd:ro
173 | - /etc/group:/etc/group:ro
174 | environment:
175 | typo3DatabaseName: func_test
176 | typo3DatabaseUsername: root
177 | typo3DatabasePassword: funcp
178 | typo3DatabaseHost: mariadb10
179 | working_dir: ${CORE_ROOT}/.Build
180 | command: >
181 | /bin/sh -c "
182 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
183 | set -x
184 | fi
185 | echo Waiting for database start...;
186 | while ! nc -z mariadb10 3306; do
187 | sleep 1;
188 | done;
189 | echo Database is up;
190 | php -v | grep '^PHP';
191 | if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
192 | XDEBUG_MODE=\"off\" \
193 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE};
194 | else
195 | DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
196 | XDEBUG_MODE=\"debug,develop\" \
197 | XDEBUG_TRIGGER=\"foo\" \
198 | XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \
199 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE};
200 | fi
201 | "
202 |
203 | functional_postgres10:
204 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
205 | user: "${HOST_UID}"
206 | links:
207 | - postgres10
208 | volumes:
209 | - ${CORE_ROOT}:${CORE_ROOT}
210 | - ${HOST_HOME}:${HOST_HOME}
211 | - /etc/passwd:/etc/passwd:ro
212 | - /etc/group:/etc/group:ro
213 | environment:
214 | typo3DatabaseDriver: pdo_pgsql
215 | typo3DatabaseName: bamboo
216 | typo3DatabaseUsername: ${HOST_USER}
217 | typo3DatabaseHost: postgres10
218 | typo3DatabasePassword: funcp
219 | working_dir: ${CORE_ROOT}/.Build
220 | command: >
221 | /bin/sh -c "
222 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
223 | set -x
224 | fi
225 | echo Waiting for database start...;
226 | while ! nc -z postgres10 5432; do
227 | sleep 1;
228 | done;
229 | echo Database is up;
230 | php -v | grep '^PHP';
231 | if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
232 | XDEBUG_MODE=\"off\" \
233 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE};
234 | else
235 | DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
236 | XDEBUG_MODE=\"debug,develop\" \
237 | XDEBUG_TRIGGER=\"foo\" \
238 | XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \
239 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE};
240 | fi
241 | "
242 |
243 | functional_sqlite:
244 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
245 | user: "${HOST_UID}"
246 | volumes:
247 | - ${CORE_ROOT}:${CORE_ROOT}
248 | - ${HOST_HOME}:${HOST_HOME}
249 | - /etc/passwd:/etc/passwd:ro
250 | - /etc/group:/etc/group:ro
251 | tmpfs:
252 | - ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid,uid=${HOST_UID}
253 | environment:
254 | typo3DatabaseDriver: pdo_sqlite
255 | working_dir: ${CORE_ROOT}/.Build
256 | command: >
257 | /bin/sh -c "
258 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
259 | set -x
260 | fi
261 | php -v | grep '^PHP';
262 | if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
263 | XDEBUG_MODE=\"off\" \
264 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE};
265 | else
266 | DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
267 | XDEBUG_MODE=\"debug,develop\" \
268 | XDEBUG_TRIGGER=\"foo\" \
269 | XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \
270 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE};
271 | fi
272 | "
273 |
274 | lint:
275 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
276 | user: "${HOST_UID}"
277 | volumes:
278 | - ${CORE_ROOT}:${CORE_ROOT}
279 | - /etc/passwd:/etc/passwd:ro
280 | - /etc/group:/etc/group:ro
281 | working_dir: ${CORE_ROOT}
282 | command: >
283 | /bin/sh -c "
284 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
285 | set -x
286 | fi
287 | php -v | grep '^PHP';
288 | find . -name \\*.php ! -path "./.Build/\\*" -print0 | xargs -0 -n1 -P4 php -dxdebug.mode=off -l >/dev/null
289 | "
290 |
291 | phpstan:
292 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
293 | user: "${HOST_UID}"
294 | volumes:
295 | - ${CORE_ROOT}:${CORE_ROOT}
296 | - ${HOST_HOME}:${HOST_HOME}
297 | - /etc/passwd:/etc/passwd:ro
298 | - /etc/group:/etc/group:ro
299 | working_dir: ${CORE_ROOT}
300 | command: >
301 | /bin/sh -c "
302 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
303 | set -x
304 | fi
305 | php -v | grep '^PHP';
306 | php -dxdebug.mode=off .Build/bin/phpstan analyze -c Build/${PHPSTAN_CONFIG_FILE} --no-progress --no-interaction
307 | "
308 |
309 | unit:
310 | image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest
311 | user: "${HOST_UID}"
312 | volumes:
313 | - ${CORE_ROOT}:${CORE_ROOT}
314 | - ${HOST_HOME}:${HOST_HOME}
315 | - /etc/passwd:/etc/passwd:ro
316 | - /etc/group:/etc/group:ro
317 | working_dir: ${CORE_ROOT}/.Build
318 | command: >
319 | /bin/sh -c "
320 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then
321 | set -x
322 | fi
323 | php -v | grep '^PHP';
324 | if [ ${PHP_XDEBUG_ON} -eq 0 ]; then
325 | XDEBUG_MODE=\"off\" \
326 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE};
327 | else
328 | DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'`
329 | XDEBUG_MODE=\"debug,develop\" \
330 | XDEBUG_TRIGGER=\"foo\" \
331 | XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \
332 | bin/phpunit -c Web/typo3conf/ext/{{EXTENSION_KEY}}/Build/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE};
333 | fi
334 | "
335 |
--------------------------------------------------------------------------------
/Resources/Private/CodeTemplates/Command.php:
--------------------------------------------------------------------------------
1 | setDescription('{{DESCRIPTION}}');
16 | }
17 |
18 | protected function execute(InputInterface $input, OutputInterface $output): int
19 | {
20 | // Do awesome stuff
21 | return 0;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Resources/Private/CodeTemplates/EventListener.php:
--------------------------------------------------------------------------------
1 | handle($request);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Resources/Public/Icons/Extension.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
57 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "b13/make",
3 | "description": "Kickstarter CLI tool for various TYPO3 functionalities",
4 | "type": "typo3-cms-extension",
5 | "homepage": "https://b13.com",
6 | "license": "GPL-2.0-or-later",
7 | "keywords": [
8 | "TYPO3",
9 | "Kickstarter",
10 | "Extensions",
11 | "CLI"
12 | ],
13 | "authors": [
14 | {
15 | "name": "Oliver Bartsch",
16 | "email": "oliver.bartsch@b13.com"
17 | }
18 | ],
19 | "require": {
20 | "typo3/cms-core": "^10.0 || ^11.0 || ^12.0 || ^13.0"
21 | },
22 | "require-dev": {
23 | "phpstan/phpstan": "^1.4",
24 | "typo3/cms-core": "^11.5",
25 | "typo3/coding-standards": "^0.5",
26 | "typo3/tailor": "^1.4",
27 | "typo3/testing-framework": "^7.0"
28 | },
29 | "config": {
30 | "vendor-dir": ".Build/vendor",
31 | "bin-dir": ".Build/bin",
32 | "sort-packages": true,
33 | "allow-plugins": {
34 | "typo3/class-alias-loader": true,
35 | "typo3/cms-composer-installers": true
36 | }
37 | },
38 | "extra": {
39 | "typo3/cms": {
40 | "extension-key": "make",
41 | "cms-package-dir": "{$vendor-dir}/typo3/cms",
42 | "web-dir": ".Build/Web"
43 | }
44 | },
45 | "scripts": {
46 | "prepare-tests-10": [
47 | "TYPO3\\TestingFramework\\Composer\\ExtensionTestEnvironment::prepare"
48 | ]
49 | },
50 | "autoload": {
51 | "psr-4": {
52 | "B13\\Make\\": "Classes/"
53 | }
54 | },
55 | "autoload-dev": {
56 | "psr-4": {
57 | "B13\\Make\\Tests\\": "Tests/"
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/ext_emconf.php:
--------------------------------------------------------------------------------
1 | 'Make',
5 | 'description' => 'Kickstarter CLI tool for various TYPO3 functionalities',
6 | 'category' => 'misc',
7 | 'author' => 'b13 GmbH',
8 | 'author_email' => 'typo3@b13.com',
9 | 'author_company' => 'b13 GmbH',
10 | 'state' => 'beta',
11 | 'clearCacheOnLoad' => true,
12 | 'version' => '0.1.8',
13 | 'constraints' => [
14 | 'depends' => [
15 | 'typo3' => '10.4.0-13.4.99',
16 | ],
17 | 'conflicts' => [],
18 | 'suggests' => [],
19 | ],
20 | 'autoload' => [
21 | 'psr-4' => [
22 | 'B13\\Make\\' => 'Classes/',
23 | ],
24 | ],
25 | ];
26 |
--------------------------------------------------------------------------------