Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Performance] Implement command loader to reduce initialization time of Magento CLI #29355

Open
wants to merge 7 commits into
base: 2.4-develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ class MaintenanceAllowIpsCommand extends AbstractSetupCommand
/**
* Names of input arguments or options
*/
const INPUT_KEY_IP = 'ip';
const INPUT_KEY_NONE = 'none';
const INPUT_KEY_ADD = 'add';
public const INPUT_KEY_IP = 'ip';
public const INPUT_KEY_NONE = 'none';
public const INPUT_KEY_ADD = 'add';
public const NAME = 'maintenance:allow-ips';

/**
* @var MaintenanceMode
Expand Down Expand Up @@ -76,7 +77,7 @@ protected function configure(): void
'Add the IP address to existing list'
),
];
$this->setName('maintenance:allow-ips')
$this->setName(self::NAME)
->setDescription('Sets maintenance mode exempt IPs')
->setDefinition(array_merge($arguments, $options));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
*/
class MaintenanceDisableCommand extends AbstractMaintenanceCommand
{
public const NAME = 'maintenance:disable';

/**
* Initialization of the command
*
* @return void
*/
protected function configure()
{
$this->setName('maintenance:disable')->setDescription('Disables maintenance mode');
$this->setName(self::NAME)->setDescription('Disables maintenance mode');

parent::configure();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
*/
class MaintenanceEnableCommand extends AbstractMaintenanceCommand
{
public const NAME = 'maintenance:enable';

/**
* Initialization of the command
*
* @return void
*/
protected function configure(): void
{
$this->setName('maintenance:enable')->setDescription('Enables maintenance mode');
$this->setName(self::NAME)->setDescription('Enables maintenance mode');

parent::configure();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
class MaintenanceStatusCommand extends AbstractSetupCommand
{
public const NAME = 'maintenance:status';

/**
* @var MaintenanceMode $maintenanceMode
*/
Expand All @@ -40,7 +42,7 @@ public function __construct(MaintenanceMode $maintenanceMode)
*/
protected function configure(): void
{
$this->setName('maintenance:status')
$this->setName(self::NAME)
->setDescription('Displays maintenance mode status');

parent::configure();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected function getAllowedNamespaces()
'Framework',
'SomeModule',
'ModuleName',
'Setup\Console\CommandList',
'Setup\Console\CommandLoader',
'Setup\Console\CompilerPreparation',
'Setup\Model\ObjectManagerProvider',
'Setup\Mvc\Bootstrap\InitParamListener',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\Framework\App\Test\Unit\Console\CommandLoader;

use Magento\Framework\Console\CommandLoader\Aggregate;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;

/**
* Tests the "aggregate" command loader
* @see Aggregate
*/
class AggregateTest extends TestCase
{
/** @var CommandLoaderInterface|MockObject */
private MockObject|CommandLoaderInterface $firstMockCommandLoader;

/** @var CommandLoaderInterface|MockObject */
private MockObject|CommandLoaderInterface $secondMockCommandLoader;

/** @var Aggregate */
private Aggregate $aggregateCommandLoader;

protected function setUp(): void
{
$this->firstMockCommandLoader = $this->getMockBuilder(CommandLoaderInterface::class)->getMock();
$this->secondMockCommandLoader = $this->getMockBuilder(CommandLoaderInterface::class)->getMock();
$this->aggregateCommandLoader = new Aggregate([$this->firstMockCommandLoader, $this->secondMockCommandLoader]);
}

/**
* Test the various cases of `has` for the aggregate command loader:
* - When at least one "internal" command loader has a command, the aggregate does as well
* - When none of the "internal" command loaders has a command, neither does the aggregate
*
* @dataProvider provideTestCasesForHas
*/
public function testHas(bool $firstResult, bool $secondResult, bool $overallResult): void
{
$this->firstMockCommandLoader->method('has')->with('foo')->willReturn($firstResult);
$this->secondMockCommandLoader->method('has')->with('foo')->willReturn($secondResult);

$this->assertEquals($overallResult, $this->aggregateCommandLoader->has('foo'));
}

public function provideTestCasesForHas(): array
{
return [
[true, false, true],
[false, true, true],
[false, false, false]
];
}

/**
* Test the various cases of `get` for the aggregate command loader. Similar to `has`,
* the return value of `Aggregate::get` mirrors its internal command loaders.
*
* For simplicity, this test does not cover the "no results" case. @see testGetThrow
*
* @dataProvider provideTestCasesForGet
*/
public function testGet(?Command $firstCmd, ?Command $secondCmd): void
{
$firstHas = (bool)$firstCmd;
$secondHas = (bool)$secondCmd;
$this->firstMockCommandLoader->method('has')->with('foo')->willReturn($firstHas);
$this->firstMockCommandLoader->method('get')->with('foo')->willReturn($firstCmd);
$this->secondMockCommandLoader->method('has')->with('foo')->willReturn($secondHas);
$this->secondMockCommandLoader->method('get')->with('foo')->willReturn($secondCmd);

$this->assertInstanceOf(Command::class, $this->aggregateCommandLoader->get('foo'));
}

public function provideTestCasesForGet(): array
{
return [
[
new Command(),
null
],
[
null,
new Command()
]
];
}

/**
* When none of the internal command loaders have matching commands, the aggregate command loader
* will throw an exception. @see CommandNotFoundException
*/
public function testGetThrow(): void
{
$this->firstMockCommandLoader->method('has')->with('foo')->willReturn(false);
$this->secondMockCommandLoader->method('has')->with('foo')->willReturn(false);

$this->expectException(CommandNotFoundException::class);
$this->aggregateCommandLoader->get('foo');
}

/**
* An aggregate command loader's `getNames` method returns the merged array of the `getNames`
* return values of all its internal command loaders
*/
public function testGetNames(): void
{
$this->firstMockCommandLoader->method('getNames')->willReturn(['foo', 'bar']);
$this->secondMockCommandLoader->method('getNames')->willReturn(['baz', 'qux']);

$this->assertEquals(['foo', 'bar', 'baz', 'qux'], $this->aggregateCommandLoader->getNames());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\Framework\App\Test\Unit\Console;

use Magento\Framework\Console\CommandLoader;
use Magento\Framework\ObjectManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;

class CommandLoaderTest extends TestCase
{
/** @var MockObject|ObjectManagerInterface */
private ObjectManagerInterface|MockObject $objectManagerMock;

protected function setUp(): void
{
$this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)->getMock();
}

/**
* Test that the command loader, when provided zero commands, does not have a command named "foo"
*/
public function testHasWithZeroCommands(): void
{
$subj = new CommandLoader($this->objectManagerMock, []);

$this->assertFalse($subj->has('foo'));
}

/**
* Test that the command loader will return true when provided with a command "foo"
*/
public function testHasWithAtLeastOneCommand(): void
{
$subj = new CommandLoader($this->objectManagerMock, [
[
'name' => 'foo',
'class' => FooCommand::class
]
]);

$this->assertTrue($subj->has('foo'));
}

/**
* Test that the command loader will throw a CommandNotFoundException when it does not have the requested command
*/
public function testGetWithZeroCommands(): void
{
$subj = new CommandLoader($this->objectManagerMock, []);

$this->expectException(CommandNotFoundException::class);

$subj->get('foo');
}

/**
* Test that the command loader returns a command when one it has is requested
*/
public function testGetWithAtLeastOneCommand(): void
{
$this->objectManagerMock
->method('create')
->with(FooCommand::class)
->willReturn(new FooCommand());

$subj = new CommandLoader($this->objectManagerMock, [
[
'name' => 'foo',
'class' => FooCommand::class
]
]);

$this->assertInstanceOf(FooCommand::class, $subj->get('foo'));
}

/**
* Test that the command loader will return an empty "names" array when it has none
*/
public function testGetNamesWithZeroCommands(): void
{
$subj = new CommandLoader($this->objectManagerMock, []);

$this->assertEquals([], $subj->getNames());
}

/**
* Test that the command loader returns an array of its command names when `getNames` is called
*/
public function testGetNames(): void
{
$subj = new CommandLoader($this->objectManagerMock, [
[
'name' => 'foo',
'class' => FooCommand::class
],
[
'name' => 'bar',
'class' => 'BarCommand'
]
]);

$this->assertEquals(['foo', 'bar'], $subj->getNames());
}
}

// phpcs:ignore PSR1.Classes.ClassDeclaration
class FooCommand extends Command
{
}
Loading