{"id":102,"date":"2026-10-30T08:00:00","date_gmt":"2026-10-30T07:00:00","guid":{"rendered":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/"},"modified":"2026-10-16T14:00:12","modified_gmt":"2026-10-16T12:00:12","slug":"symfony-console-commands-in-typo3-v14-with-ascommand","status":"publish","type":"post","link":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/","title":{"rendered":"Symfony console commands in TYPO3 v14 with #[AsCommand]"},"content":{"rendered":"<p>Hi guys,<\/p>\n<p>Every TYPO3 project I touch sooner or later needs a CLI command. A data import from a partner system, a cleanup after a migration, a report for an editor. In the old days that meant a scheduler task class with a lot of boilerplate, later an Extbase command controller. Today a TYPO3 command is just a Symfony console command, and in TYPO3 v14 you need almost no configuration for it.<\/p>\n<p>This post builds one small but real command, step by step, and ends with a functional test. The example comes from my hoja.in migration: after importing old content into TYPO3 v14, I wanted to be sure that no &#8220;External link&#8221; page is left without a link target (the old <code>pages.url<\/code> field was replaced by <code>pages.link<\/code>, Breaking #17406). The command lists such pages and can hide them.<\/p>\n<h2>Step 1: Services.yaml with autoconfiguration<\/h2>\n<p>Your extension needs the usual <code>Configuration\/Services.yaml<\/code>. With <code>autoconfigure: true<\/code>, every class that carries <code>#[AsCommand]<\/code> is registered as a console command. No extra tag needed.<\/p>\n<pre><code class=\"language-yaml\"># EXT:sitepackage\/Configuration\/Services.yaml\nservices:\n  _defaults:\n    autowire: true\n    autoconfigure: true\n    public: false\n\n  HarvisoftSitepackage:\n    resource: '..\/Classes\/*'<\/code><\/pre>\n<p>The Symfony attribute is accepted by TYPO3 since 12.4 (Important #101567). Before that you had to tag every command in <code>Services.yaml<\/code> with <code>console.command<\/code>. The old way still works, but there is no reason to write it for new code.<\/p>\n<h2>Step 2: the command class<\/h2>\n<pre><code class=\"language-php\">&lt;?php\n\ndeclare(strict_types=1);\n\nnamespace HarvisoftSitepackageCommand;\n\nuse SymfonyComponentConsoleAttributeAsCommand;\nuse SymfonyComponentConsoleCommandCommand;\nuse SymfonyComponentConsoleInputInputInterface;\nuse SymfonyComponentConsoleInputInputOption;\nuse SymfonyComponentConsoleOutputOutputInterface;\nuse SymfonyComponentConsoleStyleSymfonyStyle;\nuse TYPO3CMSCoreDatabaseConnection;\nuse TYPO3CMSCoreDatabaseConnectionPool;\nuse TYPO3CMSCoreDomainRepositoryPageRepository;\n\n#[AsCommand(\n    name: 'sitepackage:links:hide-empty',\n    description: 'Hides \"External link\" pages that have no link target.',\n)]\nfinal class HideEmptyLinkPagesCommand extends Command\n{\n    public function __construct(private readonly ConnectionPool $connectionPool)\n    {\n        parent::__construct();\n    }\n\n    protected function configure(): void\n    {\n        $this-&gt;addOption('dry-run', null, InputOption::VALUE_NONE, 'Only report, change nothing');\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $io = new SymfonyStyle($input, $output);\n        $rows = $this-&gt;findLinkPagesWithoutTarget();\n\n        if ($rows === []) {\n            $io-&gt;success('Every external link page has a target.');\n            return Command::SUCCESS;\n        }\n\n        $io-&gt;table(['uid', 'title'], array_map(\n            static fn(array $row): array =&gt; [$row['uid'], $row['title']],\n            $rows\n        ));\n\n        if ($input-&gt;getOption('dry-run')) {\n            $io-&gt;note(sprintf('%d page(s) would be hidden.', count($rows)));\n            return Command::SUCCESS;\n        }\n\n        $connection = $this-&gt;connectionPool-&gt;getConnectionForTable('pages');\n        $io-&gt;progressStart(count($rows));\n        foreach ($rows as $row) {\n            $connection-&gt;update('pages', ['hidden' =&gt; 1], ['uid' =&gt; (int)$row['uid']]);\n            $io-&gt;progressAdvance();\n        }\n        $io-&gt;progressFinish();\n        $io-&gt;success(sprintf('%d page(s) hidden.', count($rows)));\n\n        return Command::SUCCESS;\n    }\n\n    \/**\n     * @return list&lt;array{uid: int, title: string}&gt;\n     *\/\n    private function findLinkPagesWithoutTarget(): array\n    {\n        $queryBuilder = $this-&gt;connectionPool-&gt;getQueryBuilderForTable('pages');\n        \/\/ Default restrictions already skip deleted and hidden pages\n        return $queryBuilder\n            -&gt;select('uid', 'title')\n            -&gt;from('pages')\n            -&gt;where(\n                $queryBuilder-&gt;expr()-&gt;eq(\n                    'doktype',\n                    $queryBuilder-&gt;createNamedParameter(PageRepository::DOKTYPE_LINK, Connection::PARAM_INT)\n                ),\n                $queryBuilder-&gt;expr()-&gt;eq('link', $queryBuilder-&gt;createNamedParameter(''))\n            )\n            -&gt;orderBy('uid')\n            -&gt;executeQuery()\n            -&gt;fetchAllAssociative();\n    }\n}<\/code><\/pre>\n<p>Some decisions in there:<\/p>\n<ul>\n<li><strong>Constructor injection.<\/strong> <code>ConnectionPool<\/code> comes through the constructor, no <code>GeneralUtility::makeInstance()<\/code>. Do not forget <code>parent::__construct()<\/code>, otherwise Symfony never calls <code>configure()<\/code> and your options are missing.<\/li>\n<li><strong>A <code>--dry-run<\/code> option.<\/strong> Every command that changes data should have one. You run it first on production, read the table, and only then run it for real.<\/li>\n<li><strong>SymfonyStyle.<\/strong> <code>table()<\/code>, <code>progressStart()<\/code>, <code>success()<\/code> and <code>note()<\/code> give readable output for free. When the scheduler runs the command, the output is not shown to anybody, so the return code is what counts. Return <code>Command::FAILURE<\/code> when something went wrong, never just print a red line.<\/li>\n<li><strong>Direct database writes.<\/strong> For a flag like <code>hidden<\/code> on a handful of records this is fine, but it bypasses the DataHandler: no history entry, no cache clearing, no hooks. When editors must see the change in the history or other extensions react to it, use the DataHandler instead; on the CLI that needs a backend user, which you get with <code>Bootstrap::initializeBackendAuthentication()<\/code>.<\/li>\n<\/ul>\n<h2>Step 3: run it<\/h2>\n<p>After a cache flush the command is listed under its namespace:<\/p>\n<pre><code class=\"language-bash\">ddev exec vendor\/bin\/typo3 cache:flush\nddev exec vendor\/bin\/typo3 list sitepackage\nddev exec vendor\/bin\/typo3 sitepackage:links:hide-empty --dry-run<\/code><\/pre>\n<h2>Step 4: the scheduler<\/h2>\n<p>If EXT:scheduler is installed, every command is schedulable by default. You create a task of type &#8220;Execute console commands&#8221; and pick your command in the &#8220;Schedulable Command&#8221; field; arguments and options can be set after saving the task once. That is all, no separate task class.<\/p>\n<p>Some commands should never run unattended, for example an import that needs a fresh file from a person. Before v14 you had to go back to the <code>Services.yaml<\/code> tag and set <code>schedulable: false<\/code>. TYPO3 v14.0 added an attribute for this (Feature #107151):<\/p>\n<pre><code class=\"language-php\">use SymfonyComponentConsoleAttributeAsCommand;\nuse TYPO3CMSCoreAttributeAsNonSchedulableCommand;\n\n#[AsCommand('sitepackage:import:partners', 'Imports the partner list from a CSV file')]\n#[AsNonSchedulableCommand]\nfinal class ImportPartnersCommand extends Command\n{\n}<\/code><\/pre>\n<p>The core uses it itself: <code>extension:setup<\/code> carries <code>#[AsNonSchedulableCommand]<\/code>. If you do not use the scheduler at all, a plain cron entry with <code>vendor\/bin\/typo3 sitepackage:links:hide-empty<\/code> works just as well. On hoja.in there is no scheduler installed, so that would be the way.<\/p>\n<h2>Step 5: a functional test<\/h2>\n<p>A command that changes data deserves a test with a real database. The TYPO3 testing framework (version 9 supports v13 and v14) gives you a fresh instance per test case. <code>SymfonyComponentConsoleTesterCommandTester<\/code> runs the command without a shell. Commands are public services in TYPO3, so the container hands you the real, injected object:<\/p>\n<pre><code class=\"language-php\">&lt;?php\n\ndeclare(strict_types=1);\n\nnamespace HarvisoftSitepackageTestsFunctionalCommand;\n\nuse HarvisoftSitepackageCommandHideEmptyLinkPagesCommand;\nuse PHPUnitFrameworkAttributesTest;\nuse SymfonyComponentConsoleTesterCommandTester;\nuse TYPO3TestingFrameworkCoreFunctionalFunctionalTestCase;\n\nfinal class HideEmptyLinkPagesCommandTest extends FunctionalTestCase\n{\n    protected array $testExtensionsToLoad = ['harvisoft\/sitepackage'];\n\n    #[Test]\n    public function dryRunChangesNothing(): void\n    {\n        $this-&gt;importCSVDataSet(__DIR__ . '\/Fixtures\/pages.csv');\n        $tester = new CommandTester($this-&gt;get(HideEmptyLinkPagesCommand::class));\n\n        $tester-&gt;execute(['--dry-run' =&gt; true]);\n\n        $tester-&gt;assertCommandIsSuccessful();\n        self::assertStringContainsString('1 page(s) would be hidden', $tester-&gt;getDisplay());\n        $this-&gt;assertCSVDataSet(__DIR__ . '\/Fixtures\/pages.csv');\n    }\n\n    #[Test]\n    public function hidesOnlyLinkPagesWithoutTarget(): void\n    {\n        $this-&gt;importCSVDataSet(__DIR__ . '\/Fixtures\/pages.csv');\n        $tester = new CommandTester($this-&gt;get(HideEmptyLinkPagesCommand::class));\n\n        $tester-&gt;execute([]);\n\n        $tester-&gt;assertCommandIsSuccessful();\n        $this-&gt;assertCSVDataSet(__DIR__ . '\/Fixtures\/pages_hidden.csv');\n    }\n}<\/code><\/pre>\n<p>The fixture has one healthy link page and one broken one:<\/p>\n<pre><code class=\"language-csv\">\"pages\"\n,\"uid\",\"pid\",\"doktype\",\"title\",\"link\",\"hidden\"\n,1,0,1,\"Home\",\"\",0\n,2,1,3,\"Blog\",\"\/blog\/\",0\n,3,1,3,\"Old link\",\"\",0<\/code><\/pre>\n<p><code>pages_hidden.csv<\/code> is the same file with <code>hidden<\/code> set to 1 on uid 3. The first test is the one I care about most: a dry run that writes something is a nasty surprise on production.<\/p>\n<h2>Summary<\/h2>\n<ol>\n<li>Put <code>#[AsCommand]<\/code> on the class and keep <code>autoconfigure: true<\/code> in <code>Services.yaml<\/code>.<\/li>\n<li>Inject services through the constructor and call <code>parent::__construct()<\/code>.<\/li>\n<li>Add <code>--dry-run<\/code> to everything that writes.<\/li>\n<li>Return proper exit codes, because the scheduler and cron only see those.<\/li>\n<li>Mark commands that must not run unattended with <code>#[AsNonSchedulableCommand]<\/code> (v14).<\/li>\n<li>Test with <code>CommandTester<\/code> and CSV fixtures.<\/li>\n<\/ol>\n<p>Cheers!<\/p>\n<h2>Further reading<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.typo3.org\/m\/typo3\/reference-coreapi\/main\/en-us\/ApiOverview\/CommandControllers\/Index.html\">Console commands in the TYPO3 core API reference<\/a><\/li>\n<li><a href=\"https:\/\/docs.typo3.org\/c\/typo3\/cms-core\/main\/en-us\/Changelog\/14.0\/Feature-107151-AddAsNonSchedulableCommandAttribute.html\">Feature #107151: AsNonSchedulableCommand attribute<\/a><\/li>\n<li><a href=\"https:\/\/symfony.com\/doc\/current\/console.html\">Symfony console documentation<\/a><\/li>\n<li><a href=\"https:\/\/docs.typo3.org\/m\/typo3\/reference-coreapi\/main\/en-us\/Testing\/FunctionalTesting\/Index.html\">Functional testing in TYPO3<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>A small cleanup command for TYPO3 v14: #[AsCommand] with autoconfiguration, ConnectionPool injected, SymfonyStyle output, scheduler support and a functional test with CommandTester.<\/p>\n","protected":false},"author":1,"featured_media":103,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17],"tags":[37,30,32,31,24],"class_list":["post-102","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-typo3","tag-automation","tag-extension-development","tag-php-8","tag-testing","tag-typo3-v14"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>TYPO3 v14 console commands with #[AsCommand]<\/title>\n<meta name=\"description\" content=\"Write a TYPO3 v14 CLI command with #[AsCommand], constructor injection, SymfonyStyle progress output, scheduler support and a functional test.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"TYPO3 v14 console commands with #[AsCommand]\" \/>\n<meta property=\"og:description\" content=\"Write a TYPO3 v14 CLI command with #[AsCommand], constructor injection, SymfonyStyle progress output, scheduler support and a functional test.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/\" \/>\n<meta property=\"og:site_name\" content=\"Hoja Mustaffa Abdul Latheef \u2013 Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-30T07:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Hoja Mustaffa Abdul Latheef\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@hojaonline\" \/>\n<meta name=\"twitter:site\" content=\"@hojaonline\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Hoja Mustaffa Abdul Latheef\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/\"},\"author\":{\"name\":\"Hoja Mustaffa Abdul Latheef\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"headline\":\"Symfony console commands in TYPO3 v14 with #[AsCommand]\",\"datePublished\":\"2026-10-30T07:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/\"},\"wordCount\":657,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"image\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg\",\"keywords\":[\"Automation\",\"Extension development\",\"PHP 8\",\"Testing\",\"TYPO3 v14\"],\"articleSection\":[\"TYPO3\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/\",\"name\":\"TYPO3 v14 console commands with #[AsCommand]\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg\",\"datePublished\":\"2026-10-30T07:00:00+00:00\",\"description\":\"Write a TYPO3 v14 CLI command with #[AsCommand], constructor injection, SymfonyStyle progress output, scheduler support and a functional test.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#primaryimage\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg\",\"contentUrl\":\"https:\\\/\\\/hoja.in\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/10\\\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg\",\"width\":1200,\"height\":630,\"caption\":\"CLI commands the modern way\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/symfony-console-commands-in-typo3-v14-with-ascommand\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/hoja.in\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Symfony console commands in TYPO3 v14 with #[AsCommand]\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/hoja.in\\\/blog\\\/\",\"name\":\"Hoja Mustaffa Abdul Latheef \u2013 Blog\",\"description\":\"Notes from daily work with TYPO3, PHP and AI-assisted engineering\",\"publisher\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/hoja.in\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/e6fbd54c2061674d8ea3e7861d858c7b\",\"name\":\"Hoja Mustaffa Abdul Latheef\",\"logo\":{\"@id\":\"https:\\\/\\\/hoja.in\\\/blog\\\/#\\\/schema\\\/person\\\/image\\\/\"},\"description\":\"Senior TYPO3 Developer at jweiland.net, TYPO3 CMS Certified Developer. I write about TYPO3 upgrades, extension development, PHP and AI-assisted engineering with Claude Code.\",\"sameAs\":[\"https:\\\/\\\/hoja.in\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"TYPO3 v14 console commands with #[AsCommand]","description":"Write a TYPO3 v14 CLI command with #[AsCommand], constructor injection, SymfonyStyle progress output, scheduler support and a functional test.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/","og_locale":"en_US","og_type":"article","og_title":"TYPO3 v14 console commands with #[AsCommand]","og_description":"Write a TYPO3 v14 CLI command with #[AsCommand], constructor injection, SymfonyStyle progress output, scheduler support and a functional test.","og_url":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/","og_site_name":"Hoja Mustaffa Abdul Latheef \u2013 Blog","article_published_time":"2026-10-30T07:00:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg","type":"image\/jpeg"}],"author":"Hoja Mustaffa Abdul Latheef","twitter_card":"summary_large_image","twitter_creator":"@hojaonline","twitter_site":"@hojaonline","twitter_misc":{"Written by":"Hoja Mustaffa Abdul Latheef","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#article","isPartOf":{"@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/"},"author":{"name":"Hoja Mustaffa Abdul Latheef","@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"headline":"Symfony console commands in TYPO3 v14 with #[AsCommand]","datePublished":"2026-10-30T07:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/"},"wordCount":657,"commentCount":0,"publisher":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"image":{"@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#primaryimage"},"thumbnailUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg","keywords":["Automation","Extension development","PHP 8","Testing","TYPO3 v14"],"articleSection":["TYPO3"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/","url":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/","name":"TYPO3 v14 console commands with #[AsCommand]","isPartOf":{"@id":"https:\/\/hoja.in\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#primaryimage"},"image":{"@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#primaryimage"},"thumbnailUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg","datePublished":"2026-10-30T07:00:00+00:00","description":"Write a TYPO3 v14 CLI command with #[AsCommand], constructor injection, SymfonyStyle progress output, scheduler support and a functional test.","breadcrumb":{"@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#primaryimage","url":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg","contentUrl":"https:\/\/hoja.in\/blog\/wp-content\/uploads\/2026\/10\/symfony-console-commands-in-typo3-v14-with-ascommand.jpg","width":1200,"height":630,"caption":"CLI commands the modern way"},{"@type":"BreadcrumbList","@id":"https:\/\/hoja.in\/blog\/symfony-console-commands-in-typo3-v14-with-ascommand\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/hoja.in\/blog\/"},{"@type":"ListItem","position":2,"name":"Symfony console commands in TYPO3 v14 with #[AsCommand]"}]},{"@type":"WebSite","@id":"https:\/\/hoja.in\/blog\/#website","url":"https:\/\/hoja.in\/blog\/","name":"Hoja Mustaffa Abdul Latheef \u2013 Blog","description":"Notes from daily work with TYPO3, PHP and AI-assisted engineering","publisher":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/hoja.in\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/e6fbd54c2061674d8ea3e7861d858c7b","name":"Hoja Mustaffa Abdul Latheef","logo":{"@id":"https:\/\/hoja.in\/blog\/#\/schema\/person\/image\/"},"description":"Senior TYPO3 Developer at jweiland.net, TYPO3 CMS Certified Developer. I write about TYPO3 upgrades, extension development, PHP and AI-assisted engineering with Claude Code.","sameAs":["https:\/\/hoja.in\/"]}]}},"_links":{"self":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/102","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/comments?post=102"}],"version-history":[{"count":1,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/102\/revisions"}],"predecessor-version":[{"id":174,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/posts\/102\/revisions\/174"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/media\/103"}],"wp:attachment":[{"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/media?parent=102"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/categories?post=102"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hoja.in\/blog\/wp-json\/wp\/v2\/tags?post=102"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}