First Commit
This commit is contained in:
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
7.1
|
||||
---
|
||||
|
||||
* Add `DatePoint::getMicrosecond()` and `DatePoint::setMicrosecond()`
|
||||
|
||||
6.4
|
||||
---
|
||||
|
||||
* Add `DatePoint`: an immutable DateTime implementation with stricter error handling and return types
|
||||
* Throw `DateMalformedStringException`/`DateInvalidTimeZoneException` when appropriate
|
||||
* Add `$modifier` argument to the `now()` helper
|
||||
|
||||
6.3
|
||||
---
|
||||
|
||||
* Add `ClockAwareTrait` to help write time-sensitive classes
|
||||
* Add `Clock` class and `now()` function
|
||||
|
||||
6.2
|
||||
---
|
||||
|
||||
* Add the component
|
||||
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
use Psr\Clock\ClockInterface as PsrClockInterface;
|
||||
|
||||
/**
|
||||
* A global clock.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class Clock implements ClockInterface
|
||||
{
|
||||
private static ClockInterface $globalClock;
|
||||
|
||||
public function __construct(
|
||||
private readonly ?PsrClockInterface $clock = null,
|
||||
private ?\DateTimeZone $timezone = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current global clock.
|
||||
*
|
||||
* Note that you should prefer injecting a ClockInterface or using
|
||||
* ClockAwareTrait when possible instead of using this method.
|
||||
*/
|
||||
public static function get(): ClockInterface
|
||||
{
|
||||
return self::$globalClock ??= new NativeClock();
|
||||
}
|
||||
|
||||
public static function set(PsrClockInterface $clock): void
|
||||
{
|
||||
self::$globalClock = $clock instanceof ClockInterface ? $clock : new self($clock);
|
||||
}
|
||||
|
||||
public function now(): DatePoint
|
||||
{
|
||||
$now = ($this->clock ?? self::get())->now();
|
||||
|
||||
if (!$now instanceof DatePoint) {
|
||||
$now = DatePoint::createFromInterface($now);
|
||||
}
|
||||
|
||||
return isset($this->timezone) ? $now->setTimezone($this->timezone) : $now;
|
||||
}
|
||||
|
||||
public function sleep(float|int $seconds): void
|
||||
{
|
||||
$clock = $this->clock ?? self::get();
|
||||
|
||||
if ($clock instanceof ClockInterface) {
|
||||
$clock->sleep($seconds);
|
||||
} else {
|
||||
(new NativeClock())->sleep($seconds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidTimeZoneException When $timezone is invalid
|
||||
*/
|
||||
public function withTimeZone(\DateTimeZone|string $timezone): static
|
||||
{
|
||||
if (\is_string($timezone)) {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->timezone = $timezone;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
/**
|
||||
* A trait to help write time-sensitive classes.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
trait ClockAwareTrait
|
||||
{
|
||||
private readonly ClockInterface $clock;
|
||||
|
||||
#[Required]
|
||||
public function setClock(ClockInterface $clock): void
|
||||
{
|
||||
$this->clock = $clock;
|
||||
}
|
||||
|
||||
protected function now(): DatePoint
|
||||
{
|
||||
$now = ($this->clock ??= new Clock())->now();
|
||||
|
||||
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
use Psr\Clock\ClockInterface as PsrClockInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
interface ClockInterface extends PsrClockInterface
|
||||
{
|
||||
public function sleep(float|int $seconds): void;
|
||||
|
||||
public function withTimeZone(\DateTimeZone|string $timezone): static;
|
||||
}
|
||||
Vendored
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
/**
|
||||
* An immmutable DateTime with stricter error handling and return types than the native one.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class DatePoint extends \DateTimeImmutable
|
||||
{
|
||||
/**
|
||||
* @throws \DateMalformedStringException When $datetime is invalid
|
||||
*/
|
||||
public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null, ?parent $reference = null)
|
||||
{
|
||||
$now = $reference ?? Clock::get()->now();
|
||||
|
||||
if ('now' !== $datetime) {
|
||||
if (!$now instanceof static) {
|
||||
$now = static::createFromInterface($now);
|
||||
}
|
||||
|
||||
$builtInDate = new parent($datetime, $timezone ?? $now->getTimezone());
|
||||
$timezone = $builtInDate->getTimezone();
|
||||
|
||||
$now = $now->setTimezone($timezone)->modify($datetime);
|
||||
|
||||
if ('00:00:00.000000' === $builtInDate->format('H:i:s.u')) {
|
||||
$now = $now->setTime(0, 0);
|
||||
}
|
||||
} elseif (null !== $timezone) {
|
||||
$now = $now->setTimezone($timezone);
|
||||
}
|
||||
|
||||
$this->__unserialize((array) $now);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException When $format or $datetime are invalid
|
||||
*/
|
||||
public static function createFromFormat(string $format, string $datetime, ?\DateTimeZone $timezone = null): static
|
||||
{
|
||||
return parent::createFromFormat($format, $datetime, $timezone) ?: throw new \DateMalformedStringException(static::getLastErrors()['errors'][0] ?? 'Invalid date string or format.');
|
||||
}
|
||||
|
||||
public static function createFromInterface(\DateTimeInterface $object): static
|
||||
{
|
||||
return parent::createFromInterface($object);
|
||||
}
|
||||
|
||||
public static function createFromMutable(\DateTime $object): static
|
||||
{
|
||||
return parent::createFromMutable($object);
|
||||
}
|
||||
|
||||
public static function createFromTimestamp(int|float $timestamp): static
|
||||
{
|
||||
return parent::createFromTimestamp($timestamp);
|
||||
}
|
||||
|
||||
public function add(\DateInterval $interval): static
|
||||
{
|
||||
return parent::add($interval);
|
||||
}
|
||||
|
||||
public function sub(\DateInterval $interval): static
|
||||
{
|
||||
return parent::sub($interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException When $modifier is invalid
|
||||
*/
|
||||
public function modify(string $modifier): static
|
||||
{
|
||||
return parent::modify($modifier);
|
||||
}
|
||||
|
||||
public function setTimestamp(int $value): static
|
||||
{
|
||||
return parent::setTimestamp($value);
|
||||
}
|
||||
|
||||
public function setDate(int $year, int $month, int $day): static
|
||||
{
|
||||
return parent::setDate($year, $month, $day);
|
||||
}
|
||||
|
||||
public function setISODate(int $year, int $week, int $day = 1): static
|
||||
{
|
||||
return parent::setISODate($year, $week, $day);
|
||||
}
|
||||
|
||||
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
|
||||
{
|
||||
return parent::setTime($hour, $minute, $second, $microsecond);
|
||||
}
|
||||
|
||||
public function setTimezone(\DateTimeZone $timezone): static
|
||||
{
|
||||
return parent::setTimezone($timezone);
|
||||
}
|
||||
|
||||
public function getTimezone(): \DateTimeZone
|
||||
{
|
||||
return parent::getTimezone() ?: throw new \DateInvalidTimeZoneException('The DatePoint object has no timezone.');
|
||||
}
|
||||
|
||||
public function setMicrosecond(int $microsecond): static
|
||||
{
|
||||
if ($microsecond < 0 || $microsecond > 999999) {
|
||||
throw new \DateRangeError('DatePoint::setMicrosecond(): Argument #1 ($microsecond) must be between 0 and 999999, '.$microsecond.' given');
|
||||
}
|
||||
|
||||
return parent::setMicrosecond($microsecond);
|
||||
}
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2022-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
/**
|
||||
* A clock that always returns the same date, suitable for testing time-sensitive logic.
|
||||
*
|
||||
* Consider using ClockSensitiveTrait in your test cases instead of using this class directly.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class MockClock implements ClockInterface
|
||||
{
|
||||
private DatePoint $now;
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException When $now is invalid
|
||||
* @throws \DateInvalidTimeZoneException When $timezone is invalid
|
||||
*/
|
||||
public function __construct(\DateTimeImmutable|string $now = 'now', \DateTimeZone|string|null $timezone = null)
|
||||
{
|
||||
if (\is_string($timezone)) {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
if (\is_string($now)) {
|
||||
$now = new DatePoint($now, $timezone ?? new \DateTimeZone('UTC'));
|
||||
} elseif (!$now instanceof DatePoint) {
|
||||
$now = DatePoint::createFromInterface($now);
|
||||
}
|
||||
|
||||
$this->now = null !== $timezone ? $now->setTimezone($timezone) : $now;
|
||||
}
|
||||
|
||||
public function now(): DatePoint
|
||||
{
|
||||
return clone $this->now;
|
||||
}
|
||||
|
||||
public function sleep(float|int $seconds): void
|
||||
{
|
||||
if (0 >= $seconds) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = (float) $this->now->format('Uu') + $seconds * 1e6;
|
||||
$now = substr_replace(\sprintf('@%07.0F', $now), '.', -6, 0);
|
||||
$timezone = $this->now->getTimezone();
|
||||
|
||||
$this->now = DatePoint::createFromInterface(new \DateTimeImmutable($now, $timezone))->setTimezone($timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateMalformedStringException When $modifier is invalid
|
||||
*/
|
||||
public function modify(string $modifier): void
|
||||
{
|
||||
$this->now = $this->now->modify($modifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidTimeZoneException When the timezone name is invalid
|
||||
*/
|
||||
public function withTimeZone(\DateTimeZone|string $timezone): static
|
||||
{
|
||||
if (\is_string($timezone)) {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->now = $clone->now->setTimezone($timezone);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
/**
|
||||
* A monotonic clock suitable for performance profiling.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class MonotonicClock implements ClockInterface
|
||||
{
|
||||
private int $sOffset;
|
||||
private int $usOffset;
|
||||
private \DateTimeZone $timezone;
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidTimeZoneException When $timezone is invalid
|
||||
*/
|
||||
public function __construct(\DateTimeZone|string|null $timezone = null)
|
||||
{
|
||||
if (false === $offset = hrtime()) {
|
||||
throw new \RuntimeException('hrtime() returned false: the runtime environment does not provide access to a monotonic timer.');
|
||||
}
|
||||
|
||||
$time = explode(' ', microtime(), 2);
|
||||
$this->sOffset = $time[1] - $offset[0];
|
||||
$this->usOffset = (int) ($time[0] * 1000000) - (int) ($offset[1] / 1000);
|
||||
|
||||
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
|
||||
}
|
||||
|
||||
public function now(): DatePoint
|
||||
{
|
||||
[$s, $us] = hrtime();
|
||||
|
||||
if (1000000 <= $us = (int) ($us / 1000) + $this->usOffset) {
|
||||
++$s;
|
||||
$us -= 1000000;
|
||||
} elseif (0 > $us) {
|
||||
--$s;
|
||||
$us += 1000000;
|
||||
}
|
||||
|
||||
if (6 !== \strlen($now = (string) $us)) {
|
||||
$now = str_pad($now, 6, '0', \STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$now = '@'.($s + $this->sOffset).'.'.$now;
|
||||
|
||||
return DatePoint::createFromInterface(new \DateTimeImmutable($now, $this->timezone))->setTimezone($this->timezone);
|
||||
}
|
||||
|
||||
public function sleep(float|int $seconds): void
|
||||
{
|
||||
if (0 < $s = (int) $seconds) {
|
||||
sleep($s);
|
||||
}
|
||||
|
||||
if (0 < $us = $seconds - $s) {
|
||||
usleep((int) ($us * 1E6));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidTimeZoneException When $timezone is invalid
|
||||
*/
|
||||
public function withTimeZone(\DateTimeZone|string $timezone): static
|
||||
{
|
||||
if (\is_string($timezone)) {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->timezone = $timezone;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
/**
|
||||
* A clock that relies the system time.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class NativeClock implements ClockInterface
|
||||
{
|
||||
private \DateTimeZone $timezone;
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidTimeZoneException When $timezone is invalid
|
||||
*/
|
||||
public function __construct(\DateTimeZone|string|null $timezone = null)
|
||||
{
|
||||
$this->timezone = \is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
|
||||
}
|
||||
|
||||
public function now(): DatePoint
|
||||
{
|
||||
return DatePoint::createFromInterface(new \DateTimeImmutable('now', $this->timezone));
|
||||
}
|
||||
|
||||
public function sleep(float|int $seconds): void
|
||||
{
|
||||
if (0 < $s = (int) $seconds) {
|
||||
sleep($s);
|
||||
}
|
||||
|
||||
if (0 < $us = $seconds - $s) {
|
||||
usleep((int) ($us * 1E6));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \DateInvalidTimeZoneException When $timezone is invalid
|
||||
*/
|
||||
public function withTimeZone(\DateTimeZone|string $timezone): static
|
||||
{
|
||||
if (\is_string($timezone)) {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->timezone = $timezone;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
Clock Component
|
||||
===============
|
||||
|
||||
Symfony Clock decouples applications from the system clock.
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
```bash
|
||||
composer require symfony/clock
|
||||
```
|
||||
|
||||
```php
|
||||
use Symfony\Component\Clock\NativeClock;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
|
||||
class MyClockSensitiveClass
|
||||
{
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
) {
|
||||
// Only if you need to force a timezone:
|
||||
//$this->clock = $clock->withTimeZone('UTC');
|
||||
}
|
||||
|
||||
public function doSomething()
|
||||
{
|
||||
$now = $this->clock->now();
|
||||
// [...] do something with $now, which is a \DateTimeImmutable object
|
||||
|
||||
$this->clock->sleep(2.5); // Pause execution for 2.5 seconds
|
||||
}
|
||||
}
|
||||
|
||||
$clock = new NativeClock();
|
||||
$service = new MyClockSensitiveClass($clock);
|
||||
$service->doSomething();
|
||||
```
|
||||
|
||||
Sponsor
|
||||
-------
|
||||
|
||||
This package is looking for a [backer][1].
|
||||
|
||||
Help Symfony by [sponsoring][3] its development!
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/components/clock.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
|
||||
[1]: https://symfony.com/backers
|
||||
[3]: https://symfony.com/sponsor
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock;
|
||||
|
||||
if (!\function_exists(now::class)) {
|
||||
/**
|
||||
* @throws \DateMalformedStringException When the modifier is invalid
|
||||
*/
|
||||
function now(string $modifier = 'now'): DatePoint
|
||||
{
|
||||
if ('now' !== $modifier) {
|
||||
return new DatePoint($modifier);
|
||||
}
|
||||
|
||||
$now = Clock::get()->now();
|
||||
|
||||
return $now instanceof DatePoint ? $now : DatePoint::createFromInterface($now);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Clock\Test;
|
||||
|
||||
use PHPUnit\Framework\Attributes\After;
|
||||
use PHPUnit\Framework\Attributes\Before;
|
||||
use PHPUnit\Framework\Attributes\BeforeClass;
|
||||
use Symfony\Component\Clock\Clock;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
|
||||
use function Symfony\Component\Clock\now;
|
||||
|
||||
/**
|
||||
* Helps with mocking the time in your test cases.
|
||||
*
|
||||
* This trait provides one self::mockTime() method that freezes the time.
|
||||
* It restores the global clock after each test case.
|
||||
* self::mockTime() accepts either a string (eg '+1 days' or '2022-12-22'),
|
||||
* a DateTimeImmutable, or a boolean (to freeze/restore the global clock).
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
trait ClockSensitiveTrait
|
||||
{
|
||||
public static function mockTime(string|\DateTimeImmutable|bool $when = true): ClockInterface
|
||||
{
|
||||
Clock::set(match (true) {
|
||||
false === $when => self::saveClockBeforeTest(false),
|
||||
true === $when => new MockClock(),
|
||||
$when instanceof \DateTimeImmutable => new MockClock($when),
|
||||
default => new MockClock(now($when)),
|
||||
});
|
||||
|
||||
return Clock::get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @beforeClass
|
||||
*
|
||||
* @before
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[Before]
|
||||
#[BeforeClass]
|
||||
public static function saveClockBeforeTest(bool $save = true): ClockInterface
|
||||
{
|
||||
static $originalClock;
|
||||
|
||||
if ($save && $originalClock) {
|
||||
self::restoreClockAfterTest();
|
||||
}
|
||||
|
||||
return $save ? $originalClock = Clock::get() : $originalClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @after
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[After]
|
||||
protected static function restoreClockAfterTest(): void
|
||||
{
|
||||
Clock::set(self::saveClockBeforeTest(false));
|
||||
}
|
||||
}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"type": "library",
|
||||
"description": "Decouples applications from the system clock",
|
||||
"keywords": ["clock", "time", "psr20"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"provide": {
|
||||
"psr/clock-implementation": "1.0"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.4.1",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [ "Resources/now.php" ],
|
||||
"psr-4": { "Symfony\\Component\\Clock\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
+1421
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\NearMissValueResolverException;
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\ResolverNotFoundException;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver as Resolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ValueResolverInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Attribute\ValueResolver;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Cursor;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\RawInputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Contracts\Service\ServiceProviderInterface;
|
||||
|
||||
/**
|
||||
* Resolves the arguments passed to a console command.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class ArgumentResolver implements ArgumentResolverInterface
|
||||
{
|
||||
/**
|
||||
* @param iterable<mixed, ValueResolverInterface> $argumentValueResolvers
|
||||
*/
|
||||
public function __construct(
|
||||
private iterable $argumentValueResolvers = [],
|
||||
private ?ContainerInterface $namedResolvers = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getArguments(InputInterface $input, callable $command, ?\ReflectionFunctionAbstract $reflector = null): array
|
||||
{
|
||||
$reflector ??= new \ReflectionFunction($command(...));
|
||||
|
||||
$argumentReflectors = [];
|
||||
foreach ($reflector->getParameters() as $param) {
|
||||
$argumentReflectors[$param->getName()] = new ReflectionMember($param);
|
||||
}
|
||||
|
||||
$arguments = [];
|
||||
|
||||
foreach ($argumentReflectors as $argumentName => $member) {
|
||||
$argumentValueResolvers = $this->argumentValueResolvers;
|
||||
$disabledResolvers = [];
|
||||
|
||||
if ($this->namedResolvers && $attributes = $member->getAttributes(ValueResolver::class)) {
|
||||
$resolverName = null;
|
||||
foreach ($attributes as $attribute) {
|
||||
if ($attribute->disabled) {
|
||||
$disabledResolvers[$attribute->resolver] = true;
|
||||
} elseif ($resolverName) {
|
||||
throw new \LogicException(\sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $member->getName(), $member->getSourceName()));
|
||||
} else {
|
||||
$resolverName = $attribute->resolver;
|
||||
}
|
||||
}
|
||||
|
||||
if ($resolverName) {
|
||||
if (!$this->namedResolvers->has($resolverName)) {
|
||||
throw new ResolverNotFoundException($resolverName, $this->namedResolvers instanceof ServiceProviderInterface ? array_keys($this->namedResolvers->getProvidedServices()) : []);
|
||||
}
|
||||
|
||||
$argumentValueResolvers = [
|
||||
$this->namedResolvers->get($resolverName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$valueResolverExceptions = [];
|
||||
foreach ($argumentValueResolvers as $name => $resolver) {
|
||||
if (isset($disabledResolvers[\is_int($name) ? $resolver::class : $name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$count = 0;
|
||||
foreach ($resolver->resolve($argumentName, $input, $member) as $argument) {
|
||||
++$count;
|
||||
$arguments[] = $argument;
|
||||
}
|
||||
} catch (NearMissValueResolverException $e) {
|
||||
$valueResolverExceptions[] = $e;
|
||||
}
|
||||
|
||||
if (1 < $count && !$member->isVariadic()) {
|
||||
throw new \InvalidArgumentException(\sprintf('"%s::resolve()" must yield at most one value for non-variadic arguments.', get_debug_type($resolver)));
|
||||
}
|
||||
|
||||
if ($count) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// For variadic parameters with explicit input mapping, 0 values is valid
|
||||
if ($member->isVariadic() && (Argument::tryFrom($member->getMember()) || Option::tryFrom($member->getMember()))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $member->getType();
|
||||
$typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null;
|
||||
|
||||
if ($typeName && \in_array($typeName, [
|
||||
InputInterface::class,
|
||||
RawInputInterface::class,
|
||||
OutputInterface::class,
|
||||
SymfonyStyle::class,
|
||||
Cursor::class,
|
||||
\Symfony\Component\Console\Application::class,
|
||||
Command::class,
|
||||
], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reasons = array_map(static fn (NearMissValueResolverException $e) => $e->getMessage(), $valueResolverExceptions);
|
||||
if (!$reasons) {
|
||||
$reasons[] = \sprintf('The parameter has no #[Argument], #[Option], or #[MapInput] attribute, and its type "%s" cannot be auto-resolved.', $typeName ?? 'unknown');
|
||||
$reasons[] = 'Add an attribute to map this parameter to command input.';
|
||||
}
|
||||
|
||||
throw new \RuntimeException(\sprintf('Could not resolve parameter "$%s" of command "%s".'."\n\n".'Possible reasons:'."\n".' • '.implode("\n • ", $reasons), $member->getName(), $member->getSourceName()));
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<int, ValueResolverInterface>
|
||||
*/
|
||||
public static function getDefaultArgumentValueResolvers(): iterable
|
||||
{
|
||||
$builtinTypeResolver = new Resolver\BuiltinTypeValueResolver();
|
||||
$backedEnumResolver = new Resolver\BackedEnumValueResolver();
|
||||
$dateTimeResolver = new Resolver\DateTimeValueResolver();
|
||||
$inputFileResolver = new Resolver\InputFileValueResolver();
|
||||
|
||||
return [
|
||||
$backedEnumResolver,
|
||||
new Resolver\UidValueResolver(),
|
||||
$inputFileResolver,
|
||||
$builtinTypeResolver,
|
||||
new Resolver\MapInputValueResolver($builtinTypeResolver, $backedEnumResolver, $dateTimeResolver),
|
||||
$dateTimeResolver,
|
||||
new Resolver\DefaultValueResolver(),
|
||||
new Resolver\VariadicValueResolver(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver;
|
||||
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\ResolverNotFoundException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Determines the arguments for a specific Console Command.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
interface ArgumentResolverInterface
|
||||
{
|
||||
/**
|
||||
* Returns the arguments to pass to the Console Command after resolution.
|
||||
*
|
||||
* @throws \RuntimeException When no value could be provided for a required argument
|
||||
* @throws ResolverNotFoundException
|
||||
*/
|
||||
public function getArguments(InputInterface $input, callable $command, ?\ReflectionFunctionAbstract $reflector = null): array;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\Exception;
|
||||
|
||||
/**
|
||||
* Lets value resolvers tell when an argument could be under their watch but failed to be resolved.
|
||||
*
|
||||
* Throwing this exception inside `ValueResolverInterface::resolve` does not interrupt the value resolvers chain.
|
||||
*/
|
||||
final class NearMissValueResolverException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\Exception;
|
||||
|
||||
final class ResolverNotFoundException extends \RuntimeException
|
||||
{
|
||||
/**
|
||||
* @param string[] $alternatives
|
||||
*/
|
||||
public function __construct(string $name, array $alternatives = [])
|
||||
{
|
||||
$msg = \sprintf('You have requested a non-existent resolver "%s".', $name);
|
||||
if ($alternatives) {
|
||||
if (1 === \count($alternatives)) {
|
||||
$msg .= ' Did you mean this: "';
|
||||
} else {
|
||||
$msg .= ' Did you mean one of these: "';
|
||||
}
|
||||
$msg .= implode('", "', $alternatives).'"?';
|
||||
}
|
||||
|
||||
parent::__construct($msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
class TraceableArgumentResolver implements ArgumentResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ArgumentResolverInterface $resolver,
|
||||
private Stopwatch $stopwatch,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getArguments(InputInterface $input, callable $command, ?\ReflectionFunctionAbstract $reflector = null): array
|
||||
{
|
||||
$e = $this->stopwatch->start('command.get_arguments');
|
||||
|
||||
try {
|
||||
return $this->resolver->getArguments($input, $command, $reflector);
|
||||
} finally {
|
||||
$e->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Resolves a BackedEnum instance from a Command argument or option.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
|
||||
*/
|
||||
final class BackedEnumValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($argument->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveArgument($argument, $input)];
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($option->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveOption($option, $input)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolveArgument(Argument $argument, InputInterface $input): ?\BackedEnum
|
||||
{
|
||||
$value = $input->getArgument($argument->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $argument->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) && !\is_int($value)) {
|
||||
throw InvalidArgumentException::fromEnumValue($argument->name, get_debug_type($value), $argument->suggestedValues);
|
||||
}
|
||||
|
||||
return $argument->typeName::tryFrom($value)
|
||||
?? throw InvalidArgumentException::fromEnumValue($argument->name, $value, $argument->suggestedValues);
|
||||
}
|
||||
|
||||
private function resolveOption(Option $option, InputInterface $input): ?\BackedEnum
|
||||
{
|
||||
$value = $input->getOption($option->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $option->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) && !\is_int($value)) {
|
||||
throw InvalidOptionException::fromEnumValue($option->name, get_debug_type($value), $option->suggestedValues);
|
||||
}
|
||||
|
||||
return $option->typeName::tryFrom($value)
|
||||
?? throw InvalidOptionException::fromEnumValue($option->name, $value, $option->suggestedValues);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Resolves values from #[Argument] or #[Option] attributes for built-in PHP types.
|
||||
*
|
||||
* Handles: string, bool, int, float, array
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class BuiltinTypeValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($member->isVariadic()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
if (is_subclass_of($argument->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$input->getArgument($argument->name)];
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
if (is_subclass_of($option->typeName, \BackedEnum::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveOption($option, $input)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolveOption(Option $option, InputInterface $input): mixed
|
||||
{
|
||||
$value = $input->getOption($option->name);
|
||||
|
||||
if (null === $value && \in_array($option->typeName, Option::ALLOWED_UNION_TYPES, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ('array' === $option->typeName && $option->allowNull && [] === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('bool' === $option->typeName) {
|
||||
if ($option->allowNull && null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value ?? $option->default;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Symfony\Component\Console\Attribute\MapDateTime;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Resolves a \DateTime* instance as a command input argument or option.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
* @author Tim Goudriaan <tim@codedmonkey.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class DateTimeValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?ClockInterface $clock = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
$type = $member->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType || !is_a($type->getName(), \DateTimeInterface::class, true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$attribute = $member->getAttribute(MapDateTime::class);
|
||||
|
||||
$inputName = $attribute?->argument ?? $attribute?->option ?? $member->getInputName();
|
||||
|
||||
// Try to get value from argument or option
|
||||
$value = null;
|
||||
if ($input->hasArgument($inputName)) {
|
||||
$value = $input->getArgument($inputName);
|
||||
} elseif ($input->hasOption($inputName)) {
|
||||
$value = $input->getOption($inputName);
|
||||
}
|
||||
|
||||
/** @var class-string<\DateTimeImmutable>|class-string<\DateTime> $class */
|
||||
$class = \DateTimeInterface::class === $type->getName() ? \DateTimeImmutable::class : $type->getName();
|
||||
|
||||
if (!$value) {
|
||||
if ($member->isNullable()) {
|
||||
return [null];
|
||||
}
|
||||
if (!$this->clock) {
|
||||
return [new $class()];
|
||||
}
|
||||
$value = $this->clock->now();
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return [$value instanceof $class ? $value : $class::createFromInterface($value)];
|
||||
}
|
||||
|
||||
$format = $attribute?->format;
|
||||
|
||||
if (null !== $format) {
|
||||
$date = $class::createFromFormat($format, $value, $this->clock?->now()->getTimeZone());
|
||||
|
||||
if (($class::getLastErrors() ?: ['warning_count' => 0])['warning_count']) {
|
||||
$date = false;
|
||||
}
|
||||
} else {
|
||||
if (false !== filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) {
|
||||
$value = '@'.$value;
|
||||
}
|
||||
try {
|
||||
$date = new $class($value, $this->clock?->now()->getTimeZone());
|
||||
} catch (\Exception) {
|
||||
$date = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
$message = \sprintf('Invalid date given for parameter "$%s".', $argumentName);
|
||||
if ($format) {
|
||||
$message .= \sprintf(' Expected format: "%s".', $format);
|
||||
}
|
||||
$message .= ' Use #[MapDateTime(format: \'your-format\')] to specify a custom format.';
|
||||
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
|
||||
return [$date];
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Yields the default value defined in the command signature when no input value has been explicitly passed.
|
||||
*
|
||||
* @author Iltar van der Berg <kjarli@gmail.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class DefaultValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($member->hasDefaultValue()) {
|
||||
return [$member->getDefaultValue()];
|
||||
}
|
||||
|
||||
if ($member->isNullable() && !$member->isVariadic()) {
|
||||
return [null];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\File\InputFile;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class InputFileValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
$type = $member->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType || InputFile::class !== $type->getName()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
return $this->resolveValue($input->getArgument($argument->name), $member);
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
return $this->resolveValue($input->getOption($option->name), $member);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolveValue(mixed $value, ReflectionMember $member): iterable
|
||||
{
|
||||
if (!$value) {
|
||||
if ($member->isNullable()) {
|
||||
return [null];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($value instanceof InputFile) {
|
||||
return [$value];
|
||||
}
|
||||
|
||||
return [InputFile::fromPath($value)];
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\MapInput;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InputValidationFailedException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* Resolves the value of a input argument/option to an object holding the #[MapInput] attribute.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class MapInputValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ValueResolverInterface $builtinTypeResolver,
|
||||
private readonly ValueResolverInterface $backedEnumResolver,
|
||||
private readonly ValueResolverInterface $dateTimeResolver,
|
||||
private readonly ?ValidatorInterface $validator = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if (!$attribute = MapInput::tryFrom($member->getMember())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$instance = $this->resolveMapInput($attribute, $input);
|
||||
$violations = $this->validator?->validate($instance, null, $attribute->validationGroups) ?? [];
|
||||
|
||||
if (!\count($violations)) {
|
||||
return [$instance];
|
||||
}
|
||||
|
||||
$map = $this->buildPropertyToInputMap($attribute);
|
||||
$messages = [];
|
||||
foreach ($violations as $violation) {
|
||||
$path = $violation->getPropertyPath();
|
||||
$label = $map[$path] ?? $path;
|
||||
$messages[] = $label.': '.$violation->getMessage();
|
||||
}
|
||||
|
||||
throw new InputValidationFailedException(implode("\n", $messages), $violations);
|
||||
}
|
||||
|
||||
private function resolveMapInput(MapInput $mapInput, InputInterface $input): object
|
||||
{
|
||||
$instance = $mapInput->getClass()->newInstanceWithoutConstructor();
|
||||
|
||||
foreach ($mapInput->getDefinition() as $name => $spec) {
|
||||
// ignore required arguments that are not set yet (may happen in interactive mode)
|
||||
if ($spec instanceof Argument && $spec->isRequired() && \in_array($input->getArgument($spec->name), [null, []], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$instance->$name = match (true) {
|
||||
$spec instanceof Argument => $this->resolveArgumentSpec($spec, $mapInput->getClass()->getProperty($name), $input),
|
||||
$spec instanceof Option => $this->resolveOptionSpec($spec, $mapInput->getClass()->getProperty($name), $input),
|
||||
$spec instanceof MapInput => $this->resolveMapInput($spec, $input),
|
||||
};
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildPropertyToInputMap(MapInput $mapInput, string $prefix = ''): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($mapInput->getDefinition() as $propertyName => $spec) {
|
||||
$path = $prefix.$propertyName;
|
||||
$map[$path] = match (true) {
|
||||
$spec instanceof Argument => $spec->name,
|
||||
$spec instanceof Option => '--'.$spec->name,
|
||||
default => $path,
|
||||
};
|
||||
if ($spec instanceof MapInput) {
|
||||
$map += $this->buildPropertyToInputMap($spec, $path.'.');
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function resolveArgumentSpec(Argument $argument, \ReflectionProperty $property, InputInterface $input): mixed
|
||||
{
|
||||
if (is_subclass_of($argument->typeName, \BackedEnum::class)) {
|
||||
return iterator_to_array($this->backedEnumResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
if (is_a($argument->typeName, \DateTimeInterface::class, true)) {
|
||||
return iterator_to_array($this->dateTimeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
return iterator_to_array($this->builtinTypeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
private function resolveOptionSpec(Option $option, \ReflectionProperty $property, InputInterface $input): mixed
|
||||
{
|
||||
if (is_subclass_of($option->typeName, \BackedEnum::class)) {
|
||||
return iterator_to_array($this->backedEnumResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
if (is_a($option->typeName, \DateTimeInterface::class, true)) {
|
||||
return iterator_to_array($this->dateTimeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
|
||||
return iterator_to_array($this->builtinTypeResolver->resolve($property->name, $input, new ReflectionMember($property)))[0] ?? null;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\ArgumentResolver\Exception\NearMissValueResolverException;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Yields a service from a service locator keyed by command and argument name.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class ServiceValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContainerInterface $container,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
$command = $input->getFirstArgument();
|
||||
|
||||
if ($command && $this->container->has($command)) {
|
||||
$locator = $this->container->get($command);
|
||||
if ($locator instanceof ContainerInterface && $locator->has($argumentName)) {
|
||||
try {
|
||||
return [$locator->get($argumentName)];
|
||||
} catch (RuntimeException|\Throwable $e) {
|
||||
$what = \sprintf('argument $%s', $argumentName);
|
||||
$message = str_replace(\sprintf('service "%s"', $argumentName), $what, $e->getMessage());
|
||||
$what .= \sprintf(' of command "%s"', $command);
|
||||
$message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message);
|
||||
|
||||
if ($e->getMessage() === $message) {
|
||||
$message = \sprintf('Cannot resolve %s: %s', $what, $message);
|
||||
}
|
||||
|
||||
throw new NearMissValueResolverException($message, $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$type = $member->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType || $type->isBuiltin()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$typeName = $type->getName();
|
||||
|
||||
if (!$this->container->has($typeName)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$service = $this->container->get($typeName);
|
||||
|
||||
if (!$service instanceof $typeName) {
|
||||
throw new NearMissValueResolverException(\sprintf('Service "%s" exists in the container but is not an instance of "%s".', $typeName, $typeName));
|
||||
}
|
||||
|
||||
return [$service];
|
||||
} catch (\Throwable $e) {
|
||||
throw new NearMissValueResolverException(\sprintf('Cannot resolve parameter "$%s" of type "%s": %s', $argumentName, $typeName, $e->getMessage()), previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class TraceableValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ValueResolverInterface $inner,
|
||||
private Stopwatch $stopwatch,
|
||||
) {
|
||||
}
|
||||
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
$method = $this->inner::class.'::'.__FUNCTION__;
|
||||
$this->stopwatch->start($method, 'command.argument_value_resolver');
|
||||
|
||||
try {
|
||||
yield from $this->inner->resolve($argumentName, $input, $member);
|
||||
} finally {
|
||||
$this->stopwatch->stop($method);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Uid\AbstractUid;
|
||||
|
||||
/**
|
||||
* Resolves an AbstractUid instance from a Command argument or option.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class UidValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($argument->typeName, AbstractUid::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveArgument($argument, $input)];
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
if (!is_subclass_of($option->typeName, AbstractUid::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$this->resolveOption($option, $input)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolveArgument(Argument $argument, InputInterface $input): ?AbstractUid
|
||||
{
|
||||
$value = $input->getArgument($argument->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $argument->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) || !$argument->typeName::isValid($value)) {
|
||||
throw new InvalidArgumentException(\sprintf('The uid for the "%s" argument is invalid.', $argument->name));
|
||||
}
|
||||
|
||||
return $argument->typeName::fromString($value);
|
||||
}
|
||||
|
||||
private function resolveOption(Option $option, InputInterface $input): ?AbstractUid
|
||||
{
|
||||
$value = $input->getOption($option->name);
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof $option->typeName) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!\is_string($value) || !$option->typeName::isValid($value)) {
|
||||
throw new InvalidOptionException(\sprintf('The uid for the "--%s" option is invalid.', $option->name));
|
||||
}
|
||||
|
||||
return $option->typeName::fromString($value);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Responsible for resolving the value of a Command argument based on its
|
||||
* parameter metadata and the Command MapInput.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
interface ValueResolverInterface
|
||||
{
|
||||
/**
|
||||
* Returns the possible value(s) for the argument.
|
||||
*/
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable;
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\ArgumentResolver\ValueResolver;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
/**
|
||||
* Yields a variadic argument's values from the input.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class VariadicValueResolver implements ValueResolverInterface
|
||||
{
|
||||
public function resolve(string $argumentName, InputInterface $input, ReflectionMember $member): iterable
|
||||
{
|
||||
if (!$member->isVariadic()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($argument = Argument::tryFrom($member->getMember())) {
|
||||
$values = $input->getArgument($argument->name);
|
||||
|
||||
if (!\is_array($values)) {
|
||||
throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the input argument "%1$s" contains a type of "%2$s" instead.', $argument->name, get_debug_type($values)));
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($member->getMember())) {
|
||||
$values = $input->getOption($option->name);
|
||||
|
||||
if (!\is_array($values)) {
|
||||
throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the input option "--%1$s" contains a type of "%2$s" instead.', $option->name, get_debug_type($values)));
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
|
||||
class Argument
|
||||
{
|
||||
public mixed $default = null;
|
||||
public array|\Closure $suggestedValues;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @var string|class-string<\BackedEnum>
|
||||
*/
|
||||
public string $typeName = '';
|
||||
private ?int $mode = null;
|
||||
private ?InteractiveAttributeInterface $interactiveAttribute = null;
|
||||
|
||||
/**
|
||||
* Represents a console command <argument> definition.
|
||||
*
|
||||
* If unset, the `name` value will be inferred from the parameter definition.
|
||||
*
|
||||
* @param array<string|Suggestion>|callable(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
|
||||
*/
|
||||
public function __construct(
|
||||
public string $description = '',
|
||||
public string $name = '',
|
||||
array|callable $suggestedValues = [],
|
||||
) {
|
||||
$this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self
|
||||
{
|
||||
$reflection = new ReflectionMember($member);
|
||||
|
||||
if (!$self = $reflection->getAttribute(self::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $reflection->getType();
|
||||
$name = $reflection->getName();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
|
||||
}
|
||||
|
||||
$self->typeName = $type->getName();
|
||||
|
||||
if (!$self->name) {
|
||||
$self->name = (new UnicodeString($name))->kebab();
|
||||
}
|
||||
|
||||
$self->default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null;
|
||||
|
||||
$isOptional = $reflection->hasDefaultValue() || $reflection->isNullable() || $reflection->isVariadic();
|
||||
$self->mode = $isOptional ? InputArgument::OPTIONAL : InputArgument::REQUIRED;
|
||||
if ('array' === $self->typeName || $reflection->isVariadic()) {
|
||||
$self->mode |= InputArgument::IS_ARRAY;
|
||||
}
|
||||
|
||||
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $reflection->getSourceThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
|
||||
// In case that the callback is declared as a static method `[Foo::class, 'methodName']` - yet it is not callable,
|
||||
// while non-static method `[Foo $instance, 'methodName']` would be callable, we transform the callback on the fly into a non-static version.
|
||||
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
|
||||
}
|
||||
|
||||
if (is_subclass_of($self->typeName, \BackedEnum::class) && !$self->suggestedValues) {
|
||||
$self->suggestedValues = array_column($self->typeName::cases(), 'value');
|
||||
}
|
||||
|
||||
$self->interactiveAttribute = Ask::tryFrom($member, $self->name) ?? AskChoice::tryFrom($member, $self->name);
|
||||
|
||||
if ($self->interactiveAttribute && $isOptional) {
|
||||
throw new LogicException(\sprintf('The %s "$%s" argument of "%s" cannot be both interactive and optional.', $reflection->getMemberName(), $self->name, $reflection->getSourceName()));
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function toInputArgument(): InputArgument
|
||||
{
|
||||
$suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
|
||||
|
||||
return new InputArgument($this->name, $this->mode, $this->description, $this->default, $suggestedValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getInteractiveAttribute(): ?InteractiveAttributeInterface
|
||||
{
|
||||
return $this->interactiveAttribute;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return InputArgument::REQUIRED === (InputArgument::REQUIRED & $this->mode);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure commands.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
|
||||
final class AsCommand
|
||||
{
|
||||
/**
|
||||
* @param string $name The name of the command, used when calling it (i.e. "cache:clear")
|
||||
* @param string|null $description The description of the command, displayed with the help page
|
||||
* @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean")
|
||||
* @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command
|
||||
* @param string|null $help The help content of the command, displayed with the help page
|
||||
* @param string[] $usages The list of usage examples, displayed with the help page
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $description = null,
|
||||
array $aliases = [],
|
||||
bool $hidden = false,
|
||||
public ?string $help = null,
|
||||
public array $usages = [],
|
||||
) {
|
||||
if (!$hidden && !$aliases) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = explode('|', $name);
|
||||
$name = array_merge($name, $aliases);
|
||||
|
||||
if ($hidden && '' !== $name[0]) {
|
||||
array_unshift($name, '');
|
||||
}
|
||||
|
||||
$this->name = implode('|', $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure targeted value resolvers.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class AsTargetedValueResolver
|
||||
{
|
||||
/**
|
||||
* @param string|null $name The name with which the resolver can be targeted
|
||||
*/
|
||||
public function __construct(public readonly ?string $name = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\File\InputFile;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\FileQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
|
||||
class Ask implements InteractiveAttributeInterface
|
||||
{
|
||||
public ?\Closure $normalizer;
|
||||
public ?\Closure $validator;
|
||||
private \Closure $closure;
|
||||
|
||||
/**
|
||||
* @param string $question The question to ask the user
|
||||
* @param string|bool|int|float|null $default The default answer to return if the user enters nothing
|
||||
* @param bool $hidden Whether the user response must be hidden or not
|
||||
* @param bool $multiline Whether the user response should accept newline characters
|
||||
* @param bool $trimmable Whether the user response must be trimmed or not
|
||||
* @param int|null $timeout The maximum time the user has to answer the question in seconds
|
||||
* @param callable|null $validator The validator for the question
|
||||
* @param int|null $maxAttempts The maximum number of attempts allowed to answer the question.
|
||||
* Null means an unlimited number of attempts
|
||||
*/
|
||||
public function __construct(
|
||||
public string $question,
|
||||
public string|bool|int|float|null $default = null,
|
||||
public bool $hidden = false,
|
||||
public bool $multiline = false,
|
||||
public bool $trimmable = true,
|
||||
public ?int $timeout = null,
|
||||
?callable $normalizer = null,
|
||||
?callable $validator = null,
|
||||
public ?int $maxAttempts = null,
|
||||
/** @var Constraint[] */
|
||||
public array $constraints = [],
|
||||
) {
|
||||
$this->normalizer = $normalizer ? $normalizer(...) : null;
|
||||
$this->validator = $validator ? $validator(...) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member, string $name): ?self
|
||||
{
|
||||
$reflection = new ReflectionMember($member);
|
||||
|
||||
if (!$self = $reflection->getAttribute(self::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $reflection->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for interactive questions.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
|
||||
}
|
||||
|
||||
$self->closure = function (SymfonyStyle $io, InputInterface $input) use ($self, $reflection, $name, $type) {
|
||||
if ($reflection->isProperty() && isset($this->{$reflection->getName()})) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($reflection->isParameter() && !\in_array($input->getArgument($name), [null, []], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$typeName = $type->getName();
|
||||
|
||||
if (InputFile::class === $typeName) {
|
||||
$question = new FileQuestion($self->question);
|
||||
$question->setValidator($self->validator);
|
||||
$question->setMaxAttempts($self->maxAttempts);
|
||||
$question->setConstraints($self->constraints);
|
||||
$value = $io->askQuestion($question);
|
||||
|
||||
if (null === $value && !$reflection->isNullable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($reflection->isProperty()) {
|
||||
$this->{$reflection->getName()} = $value;
|
||||
} else {
|
||||
$input->setArgument($name, $value);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ('bool' === $typeName) {
|
||||
$self->default ??= false;
|
||||
|
||||
if (!\is_bool($self->default)) {
|
||||
throw new LogicException(\sprintf('The "%s::$default" value for the %s "$%s" of "%s" must be a boolean.', self::class, $reflection->getMemberName(), $name, $reflection->getSourceName()));
|
||||
}
|
||||
|
||||
$question = new ConfirmationQuestion($self->question, $self->default);
|
||||
} else {
|
||||
$question = new Question($self->question, $self->default);
|
||||
}
|
||||
$question->setHidden($self->hidden);
|
||||
$question->setMultiline($self->multiline);
|
||||
$question->setTrimmable($self->trimmable);
|
||||
$question->setTimeout($self->timeout);
|
||||
|
||||
if (!$self->validator && $reflection->isProperty() && 'array' !== $typeName) {
|
||||
$self->validator = fn (mixed $value): mixed => $this->{$reflection->getName()} = $value;
|
||||
}
|
||||
|
||||
$question->setValidator($self->validator);
|
||||
$question->setMaxAttempts($self->maxAttempts);
|
||||
$question->setConstraints($self->constraints);
|
||||
|
||||
if ($self->normalizer) {
|
||||
$question->setNormalizer($self->normalizer);
|
||||
} elseif (is_subclass_of($typeName, \BackedEnum::class)) {
|
||||
/** @var class-string<\BackedEnum> $backedType */
|
||||
$backedType = $reflection->getType()->getName();
|
||||
$question->setNormalizer(static fn (string|int $value) => $backedType::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($reflection->getName(), $value, array_column($backedType::cases(), 'value')));
|
||||
}
|
||||
|
||||
if ('array' === $typeName) {
|
||||
$value = [];
|
||||
while ($v = $io->askQuestion($question)) {
|
||||
if ("\x4" === $v || \PHP_EOL === $v || ($question->isTrimmable() && '' === $v = trim($v))) {
|
||||
break;
|
||||
}
|
||||
$value[] = $v;
|
||||
}
|
||||
} else {
|
||||
$value = $io->askQuestion($question);
|
||||
}
|
||||
|
||||
if (null === $value && !$reflection->isNullable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($reflection->isProperty()) {
|
||||
$this->{$reflection->getName()} = $value;
|
||||
} else {
|
||||
$input->setArgument($name, $value);
|
||||
}
|
||||
};
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getFunction(object $instance): \ReflectionFunction
|
||||
{
|
||||
return new \ReflectionFunction($this->closure->bindTo($instance, $instance::class));
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
|
||||
class AskChoice implements InteractiveAttributeInterface
|
||||
{
|
||||
public ?\Closure $validator;
|
||||
public array|\Closure $choices;
|
||||
private \Closure $closure;
|
||||
|
||||
/**
|
||||
* @param string $question The question to ask the user
|
||||
* @param array<string|int|float>|callable():array<string|int|float> $choices The list of available choices (leave empty to use enum cases)
|
||||
* @param string|int|float|null $default The default answer to return if the user enters nothing
|
||||
* @param string $errorMessage The error message when the answer is invalid
|
||||
* @param string $prompt The prompt displayed before the user input
|
||||
* @param callable|null $validator The validator for the answer
|
||||
* @param int|null $maxAttempts The maximum number of attempts allowed to answer the question.
|
||||
* Null means an unlimited number of attempts
|
||||
*/
|
||||
public function __construct(
|
||||
public string $question,
|
||||
array|callable $choices = [],
|
||||
public string|int|float|null $default = null,
|
||||
public string $errorMessage = 'Value "%s" is invalid',
|
||||
public string $prompt = ' > ',
|
||||
?callable $validator = null,
|
||||
public ?int $maxAttempts = null,
|
||||
) {
|
||||
$this->validator = $validator ? $validator(...) : null;
|
||||
$this->choices = \is_callable($choices) ? $choices(...) : $choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member, string $name): ?self
|
||||
{
|
||||
$reflection = new ReflectionMember($member);
|
||||
|
||||
if (!$self = $reflection->getAttribute(self::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $reflection->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for choice questions.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
|
||||
}
|
||||
|
||||
$isBackedEnum = is_subclass_of($type->getName(), \BackedEnum::class);
|
||||
|
||||
// Validate that choices are provided or can be derived from enum
|
||||
if (!$self->choices && !$isBackedEnum) {
|
||||
throw new LogicException(\sprintf('The #[AskChoice] attribute for the %s "$%s" of "%s" requires either explicit choices or a BackedEnum type.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
|
||||
}
|
||||
|
||||
$self->closure = function (SymfonyStyle $io, InputInterface $input) use ($self, $reflection, $name, $type, $isBackedEnum) {
|
||||
if ($reflection->isProperty() && isset($this->{$reflection->getName()})) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($reflection->isParameter() && !\in_array($input->getArgument($name), [null, []], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$choices = $self->choices instanceof \Closure ? ($self->choices)() : $self->choices;
|
||||
|
||||
// Derive choices from enum cases if not provided
|
||||
if (!$choices && $isBackedEnum) {
|
||||
/** @var class-string<\BackedEnum> $enumClass */
|
||||
$enumClass = $type->getName();
|
||||
$choices = array_column($enumClass::cases(), 'value');
|
||||
}
|
||||
|
||||
$question = new ChoiceQuestion($self->question, $choices, $self->default);
|
||||
$question->setMultiselect('array' === $type->getName());
|
||||
$question->setErrorMessage($self->errorMessage);
|
||||
$question->setPrompt($self->prompt);
|
||||
$question->setMaxAttempts($self->maxAttempts);
|
||||
|
||||
if (!$self->validator && $reflection->isProperty() && !$isBackedEnum && 'array' !== $type->getName()) {
|
||||
$self->validator = fn (mixed $value): mixed => $this->{$reflection->getName()} = $value;
|
||||
}
|
||||
|
||||
if ($self->validator) {
|
||||
$question->setValidator($self->validator);
|
||||
}
|
||||
|
||||
$value = $io->askQuestion($question);
|
||||
|
||||
if (null === $value && !$reflection->isNullable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert back to enum if needed
|
||||
if ($isBackedEnum) {
|
||||
/** @var class-string<\BackedEnum> $enumClass */
|
||||
$enumClass = $type->getName();
|
||||
if ($question->isMultiselect() && \is_array($value)) {
|
||||
$value = array_map(static fn ($v) => $enumClass::from($v), $value);
|
||||
} else {
|
||||
$value = $enumClass::from($value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($reflection->isProperty()) {
|
||||
$this->{$reflection->getName()} = $value;
|
||||
} else {
|
||||
$input->setArgument($name, $value);
|
||||
}
|
||||
};
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getFunction(object $instance): \ReflectionFunction
|
||||
{
|
||||
return new \ReflectionFunction($this->closure->bindTo($instance, $instance::class));
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class Interact implements InteractiveAttributeInterface
|
||||
{
|
||||
private \ReflectionMethod $method;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function tryFrom(\ReflectionMethod $method): ?self
|
||||
{
|
||||
/** @var self|null $self */
|
||||
if (!$self = ($method->getAttributes(self::class)[0] ?? null)?->newInstance()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$method->isPublic() || $method->isStatic()) {
|
||||
throw new LogicException(\sprintf('The interactive method "%s::%s()" must be public and non-static.', $method->class, $method->getName()));
|
||||
}
|
||||
|
||||
if ('__invoke' === $method->getName()) {
|
||||
throw new LogicException(\sprintf('The "%s::__invoke()" method cannot be used as an interactive method.', $method->class));
|
||||
}
|
||||
|
||||
$self->method = $method;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getFunction(object $instance): \ReflectionFunction
|
||||
{
|
||||
return new \ReflectionFunction($this->method->getClosure($instance));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface InteractiveAttributeInterface
|
||||
{
|
||||
public function getFunction(object $instance): \ReflectionFunction;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
/**
|
||||
* Defines how a DateTime parameter should be resolved from command input.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||
class MapDateTime
|
||||
{
|
||||
/**
|
||||
* @param string|null $format The DateTime format (@see https://php.net/datetime.format)
|
||||
* @param string|null $argument The argument name to read from (defaults to parameter name)
|
||||
* @param string|null $option The option name to read from (mutually exclusive with $argument)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?string $format = null,
|
||||
public readonly ?string $argument = null,
|
||||
public readonly ?string $option = null,
|
||||
) {
|
||||
if ($argument && $option) {
|
||||
throw new \LogicException('MapDateTime cannot specify both argument and option.');
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Interaction\Interaction;
|
||||
|
||||
/**
|
||||
* Maps a command input into an object (DTO).
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
|
||||
final class MapInput
|
||||
{
|
||||
/**
|
||||
* @var array<string, Argument|Option|self>
|
||||
*/
|
||||
private array $definition = [];
|
||||
|
||||
private \ReflectionClass $class;
|
||||
|
||||
/**
|
||||
* @var list<Interact>
|
||||
*/
|
||||
private array $interactiveAttributes = [];
|
||||
|
||||
/**
|
||||
* @param string[]|null $validationGroups
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?array $validationGroups = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self
|
||||
{
|
||||
$reflection = new ReflectionMember($member);
|
||||
|
||||
if (!$self = $reflection->getAttribute(self::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $reflection->getType();
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
throw new LogicException(\sprintf('The input %s "%s" must have a named type.', $reflection->getMemberName(), $member->name));
|
||||
}
|
||||
|
||||
if (!class_exists($class = $type->getName())) {
|
||||
throw new LogicException(\sprintf('The input class "%s" does not exist.', $type->getName()));
|
||||
}
|
||||
|
||||
$self->class = new \ReflectionClass($class);
|
||||
|
||||
foreach ($self->class->getProperties() as $property) {
|
||||
if ($argument = Argument::tryFrom($property)) {
|
||||
$self->definition[$property->name] = $argument;
|
||||
} elseif ($option = Option::tryFrom($property)) {
|
||||
$self->definition[$property->name] = $option;
|
||||
} elseif ($input = self::tryFrom($property)) {
|
||||
$self->definition[$property->name] = $input;
|
||||
}
|
||||
|
||||
if (isset($self->definition[$property->name]) && (!$property->isPublic() || $property->isStatic())) {
|
||||
throw new LogicException(\sprintf('The input property "%s::$%s" must be public and non-static.', $self->class->name, $property->name));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$self->definition) {
|
||||
throw new LogicException(\sprintf('The input class "%s" must have at least one argument or option.', $self->class->name));
|
||||
}
|
||||
|
||||
foreach ($self->class->getMethods() as $method) {
|
||||
if ($attribute = Interact::tryFrom($method)) {
|
||||
$self->interactiveAttributes[] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setValue(InputInterface $input, object $object): void
|
||||
{
|
||||
foreach ($this->definition as $name => $spec) {
|
||||
$property = $this->class->getProperty($name);
|
||||
|
||||
if (!$property->isInitialized($object) || \in_array($value = $property->getValue($object), [null, []], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match (true) {
|
||||
$spec instanceof Argument => $input->setArgument($spec->name, $value),
|
||||
$spec instanceof Option => $input->setOption($spec->name, $value),
|
||||
$spec instanceof self => $spec->setValue($input, $value),
|
||||
default => throw new LogicException('Unexpected specification type.'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Argument>
|
||||
*/
|
||||
public function getArguments(): iterable
|
||||
{
|
||||
foreach ($this->definition as $spec) {
|
||||
if ($spec instanceof Argument) {
|
||||
yield $spec;
|
||||
} elseif ($spec instanceof self) {
|
||||
yield from $spec->getArguments();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Option>
|
||||
*/
|
||||
public function getOptions(): iterable
|
||||
{
|
||||
foreach ($this->definition as $spec) {
|
||||
if ($spec instanceof Option) {
|
||||
yield $spec;
|
||||
} elseif ($spec instanceof self) {
|
||||
yield from $spec->getOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return \ReflectionClass<object>
|
||||
*/
|
||||
public function getClass(): \ReflectionClass
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<string, Argument|Option|self>
|
||||
*/
|
||||
public function getDefinition(): array
|
||||
{
|
||||
return $this->definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a populated instance of the DTO from command input.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function createInstance(InputInterface $input): object
|
||||
{
|
||||
$instance = $this->class->newInstanceWithoutConstructor();
|
||||
|
||||
foreach ($this->definition as $name => $spec) {
|
||||
if ($spec instanceof Argument) {
|
||||
$value = $input->getArgument($spec->name);
|
||||
if ($spec->isRequired() && \in_array($value, [null, []], true)) {
|
||||
continue;
|
||||
}
|
||||
$instance->$name = $this->resolveValue($spec->typeName, $value, $spec->default);
|
||||
} elseif ($spec instanceof Option) {
|
||||
$value = $input->getOption($spec->name);
|
||||
$instance->$name = $this->resolveValue($spec->typeName, $value, $spec->default);
|
||||
} elseif ($spec instanceof self) {
|
||||
$instance->$name = $spec->createInstance($input);
|
||||
}
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
private function resolveValue(string $typeName, mixed $value, mixed $default): mixed
|
||||
{
|
||||
if (null === $value) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ('' === $value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_subclass_of($typeName, \BackedEnum::class)) {
|
||||
return $value instanceof $typeName ? $value : $typeName::tryFrom($value);
|
||||
}
|
||||
|
||||
if (is_a($typeName, \DateTimeInterface::class, true)) {
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value;
|
||||
}
|
||||
$class = \DateTimeInterface::class === $typeName ? \DateTimeImmutable::class : $typeName;
|
||||
|
||||
return new $class($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return iterable<Interaction>
|
||||
*/
|
||||
public function getPropertyInteractions(): iterable
|
||||
{
|
||||
foreach ($this->definition as $spec) {
|
||||
if ($spec instanceof self) {
|
||||
yield from $spec->getPropertyInteractions();
|
||||
} elseif ($spec instanceof Argument && $attribute = $spec->getInteractiveAttribute()) {
|
||||
yield new Interaction($this, $attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return iterable<Interaction>
|
||||
*/
|
||||
public function getMethodInteractions(): iterable
|
||||
{
|
||||
foreach ($this->definition as $spec) {
|
||||
if ($spec instanceof self) {
|
||||
yield from $spec->getMethodInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->interactiveAttributes as $attribute) {
|
||||
yield new Interaction($this, $attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
|
||||
class Option
|
||||
{
|
||||
public const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
|
||||
public mixed $default = null;
|
||||
public array|\Closure $suggestedValues;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @var string|class-string<\BackedEnum>
|
||||
*/
|
||||
public string $typeName = '';
|
||||
/** @internal */
|
||||
public bool $allowNull = false;
|
||||
private ?int $mode = null;
|
||||
private string $memberName = '';
|
||||
private string $sourceName = '';
|
||||
|
||||
/**
|
||||
* Represents a console command --option definition.
|
||||
*
|
||||
* If unset, the `name` value will be inferred from the parameter definition.
|
||||
*
|
||||
* @param array|string|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
||||
* @param array<string|Suggestion>|callable(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
|
||||
*/
|
||||
public function __construct(
|
||||
public string $description = '',
|
||||
public string $name = '',
|
||||
public array|string|null $shortcut = null,
|
||||
array|callable $suggestedValues = [],
|
||||
) {
|
||||
$this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self
|
||||
{
|
||||
$reflection = new ReflectionMember($member);
|
||||
|
||||
if (!$self = $reflection->getAttribute(self::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$self->memberName = $reflection->getMemberName();
|
||||
$self->sourceName = $reflection->getSourceName();
|
||||
|
||||
$name = $reflection->getName();
|
||||
$type = $reflection->getType();
|
||||
|
||||
// Variadic parameters implicitly default to an empty array
|
||||
if (!$reflection->isVariadic() && !$reflection->hasDefaultValue()) {
|
||||
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must declare a default value.', $self->memberName, $name, $self->sourceName));
|
||||
}
|
||||
|
||||
if (!$self->name) {
|
||||
$self->name = (new UnicodeString($name))->kebab();
|
||||
}
|
||||
|
||||
$self->default = $reflection->isVariadic() ? [] : $reflection->getDefaultValue();
|
||||
$self->allowNull = $reflection->isNullable();
|
||||
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
return $self->handleUnion($type);
|
||||
}
|
||||
|
||||
if (!$type instanceof \ReflectionNamedType) {
|
||||
throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped or Intersection types are not supported for command options.', $self->memberName, $name, $self->sourceName));
|
||||
}
|
||||
|
||||
$self->typeName = $type->getName();
|
||||
|
||||
if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
|
||||
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must not be nullable when it has a default boolean value.', $self->memberName, $name, $self->sourceName));
|
||||
}
|
||||
|
||||
if ($self->allowNull && null !== $self->default) {
|
||||
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must either be not-nullable or have a default of null.', $self->memberName, $name, $self->sourceName));
|
||||
}
|
||||
|
||||
if ('bool' === $self->typeName) {
|
||||
$self->mode = InputOption::VALUE_NONE;
|
||||
if (false !== $self->default) {
|
||||
$self->mode |= InputOption::VALUE_NEGATABLE;
|
||||
}
|
||||
} elseif ('array' === $self->typeName || $reflection->isVariadic()) {
|
||||
$self->mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY;
|
||||
} else {
|
||||
$self->mode = InputOption::VALUE_REQUIRED;
|
||||
}
|
||||
|
||||
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $reflection->getSourceThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
|
||||
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
|
||||
}
|
||||
|
||||
if (is_subclass_of($self->typeName, \BackedEnum::class) && !$self->suggestedValues) {
|
||||
$self->suggestedValues = array_column($self->typeName::cases(), 'value');
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function toInputOption(): InputOption
|
||||
{
|
||||
$default = InputOption::VALUE_NONE === (InputOption::VALUE_NONE & $this->mode) ? null : $this->default;
|
||||
$suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
|
||||
|
||||
return new InputOption($this->name, $this->shortcut, $this->mode, $this->description, $default, $suggestedValues);
|
||||
}
|
||||
|
||||
private function handleUnion(\ReflectionUnionType $type): self
|
||||
{
|
||||
$types = array_map(
|
||||
static fn (\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null,
|
||||
$type->getTypes(),
|
||||
);
|
||||
|
||||
sort($types);
|
||||
|
||||
$this->typeName = implode('|', array_filter($types));
|
||||
|
||||
if (!\in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
|
||||
throw new LogicException(\sprintf('The union type for %s "$%s" of "%s" is not supported as a command option. Only "%s" types are allowed.', $this->memberName, $this->name, $this->sourceName, implode('", "', self::ALLOWED_UNION_TYPES)));
|
||||
}
|
||||
|
||||
if (false !== $this->default) {
|
||||
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must have a default value of false.', $this->memberName, $this->name, $this->sourceName));
|
||||
}
|
||||
|
||||
$this->mode = InputOption::VALUE_OPTIONAL;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute\Reflection;
|
||||
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ReflectionMember
|
||||
{
|
||||
public function __construct(
|
||||
private readonly \ReflectionParameter|\ReflectionProperty $member,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T> $class
|
||||
*
|
||||
* @return T|null
|
||||
*/
|
||||
public function getAttribute(string $class): ?object
|
||||
{
|
||||
return ($this->member->getAttributes($class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @param class-string<T> $class
|
||||
*
|
||||
* @return list<T>
|
||||
*/
|
||||
public function getAttributes(string $class): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (\ReflectionAttribute $attribute) => $attribute->newInstance(),
|
||||
$this->member->getAttributes($class, \ReflectionAttribute::IS_INSTANCEOF)
|
||||
);
|
||||
}
|
||||
|
||||
public function getSourceName(): string
|
||||
{
|
||||
if ($this->member instanceof \ReflectionProperty) {
|
||||
return $this->member->class;
|
||||
}
|
||||
|
||||
$function = $this->member->getDeclaringFunction();
|
||||
|
||||
if ($function instanceof \ReflectionMethod) {
|
||||
return $function->class.'::'.$function->name.'()';
|
||||
}
|
||||
|
||||
return $function->name.'()';
|
||||
}
|
||||
|
||||
public function getSourceThis(): ?object
|
||||
{
|
||||
if ($this->member instanceof \ReflectionParameter) {
|
||||
return $this->member->getDeclaringFunction()->getClosureThis();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getType(): ?\ReflectionType
|
||||
{
|
||||
return $this->member->getType();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->member->getName();
|
||||
}
|
||||
|
||||
public function hasDefaultValue(): bool
|
||||
{
|
||||
if ($this->member instanceof \ReflectionParameter) {
|
||||
return $this->member->isDefaultValueAvailable();
|
||||
}
|
||||
|
||||
return $this->member->hasDefaultValue();
|
||||
}
|
||||
|
||||
public function getDefaultValue(): mixed
|
||||
{
|
||||
$defaultValue = $this->member->getDefaultValue();
|
||||
|
||||
if ($defaultValue instanceof \BackedEnum) {
|
||||
return $defaultValue->value;
|
||||
}
|
||||
|
||||
return $defaultValue;
|
||||
}
|
||||
|
||||
public function isNullable(): bool
|
||||
{
|
||||
return (bool) $this->member->getType()?->allowsNull();
|
||||
}
|
||||
|
||||
public function getMemberName(): string
|
||||
{
|
||||
return $this->member instanceof \ReflectionParameter ? 'parameter' : 'property';
|
||||
}
|
||||
|
||||
public function isParameter(): bool
|
||||
{
|
||||
return $this->member instanceof \ReflectionParameter;
|
||||
}
|
||||
|
||||
public function isVariadic(): bool
|
||||
{
|
||||
return $this->member instanceof \ReflectionParameter && $this->member->isVariadic();
|
||||
}
|
||||
|
||||
public function isProperty(): bool
|
||||
{
|
||||
return $this->member instanceof \ReflectionProperty;
|
||||
}
|
||||
|
||||
public function getMember(): \ReflectionParameter|\ReflectionProperty
|
||||
{
|
||||
return $this->member;
|
||||
}
|
||||
|
||||
public function getInputName(): string
|
||||
{
|
||||
return (new UnicodeString($this->member->getName()))->kebab()->toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Attribute;
|
||||
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ValueResolverInterface;
|
||||
|
||||
/**
|
||||
* Defines which value resolver should be used for a given parameter.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)]
|
||||
class ValueResolver
|
||||
{
|
||||
/**
|
||||
* @param class-string<ValueResolverInterface>|string $resolver The class name of the resolver to use
|
||||
* @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases
|
||||
*/
|
||||
public function __construct(
|
||||
public string $resolver,
|
||||
public bool $disabled = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Vendored
+351
@@ -0,0 +1,351 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
8.1
|
||||
---
|
||||
|
||||
* Add `ConsoleBundle` for console applications with DI, autodiscovery and autowiring
|
||||
* Pad styled `SymfonyStyle` blocks with the ECH ANSI sequence on decorated outputs so trailing cells are excluded from copy selections
|
||||
* Add optional `$container` parameter to `Application` for automatic service wiring from a PSR container
|
||||
* Add `SymfonyStyle::outlineBlock()` and convenience methods `outlineSuccess()`, `outlineError()`, `outlineWarning()`, `outlineNote()`, `outlineInfo()`, `outlineCaution()` for border-only message blocks with the type label embedded in the top border
|
||||
* Add `TraceableValueResolver` to help inspecting value resolvers performances
|
||||
* [BC BREAK] Add `object` support to input options and arguments' default by changing the `$default` type to `mixed` in `InputArgument`, `InputOption`, `#[Argument]` and `#[Option]`
|
||||
* Add support for pasting images with `#[Ask]` on `InputFile` types, supporting Kitty Graphics and iTerm2 protocols
|
||||
* Add `FileQuestion`, `InputFile`, `InputFileValueResolver`, and `SymfonyStyle::askFile()` for file input handling
|
||||
* Add `Question::setConstraints()` and `ValidateQuestionInputListener` to validate question input using Validator constraints
|
||||
* Add `#[AskChoice]` attribute for interactive choice questions in invokable commands
|
||||
* Add support for method-based commands with `#[AsCommand]` attribute
|
||||
* Add argument resolver support
|
||||
* Add `BackedEnum` and `DateTimeInterface` support to `#[MapInput]`
|
||||
* Add validation constraints support to `#[MapInput]` along with optional `validationGroups` to control which groups are validated
|
||||
* Add `TesterTrait::assertCommandFailed()` to test command
|
||||
* Add `TesterTrait::assertCommandIsInvalid()` to test command
|
||||
* Add a result-based testing API with `CommandTester::run()`, `ExecutionResult`, and `ConsoleAssertionsTrait` to assert output and error streams together
|
||||
* Add optional `$format` argument to `SymfonyStyle::createProgressBar()`, `SymfonyStyle::progressStart()`, and `SymfonyStyle::progressIterate()` to allow passing a custom `ProgressBar` format string
|
||||
* Allow setting a boolean default value on `InputOption::VALUE_NEGATABLE` options
|
||||
* Deprecate passing both `InputArgument::REQUIRED` and `InputArgument::OPTIONAL` modes to `InputArgument` constructor
|
||||
* Deprecate passing more than one out of `InputOption::VALUE_NONE`, `InputOption::VALUE_REQUIRED` and `InputOption::VALUE_OPTIONAL` modes to `InputOption` constructor
|
||||
* Add `RawInputInterface` to expose the original arguments and options and to unparse options, implemented by `Input`
|
||||
* Add support for OSC 9;4 for progress reporting
|
||||
|
||||
8.0
|
||||
---
|
||||
|
||||
* Make `AsCommand` attribute class `final`
|
||||
* Remove methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute
|
||||
* Ensure closures set via `Command::setCode()` method have proper parameter and return types
|
||||
* Add method `isSilent()` to `OutputInterface`
|
||||
* Remove deprecated `Symfony\Component\Console\Application::add()` method in favor of `Symfony\Component\Console\Application::addCommand()`
|
||||
* Add argument `$finishedIndicator` to `ProgressIndicator::finish()`
|
||||
|
||||
7.4
|
||||
---
|
||||
|
||||
* Add `Command::getCode()` to get the code set via `setCode()`
|
||||
* Allow setting aliases and the hidden flag via the command name passed to the constructor
|
||||
* Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone
|
||||
* Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()`
|
||||
* Add `BackedEnum` support with `#[Argument]` and `#[Option]` inputs in invokable commands
|
||||
* Allow Usages to be specified via `#[AsCommand]` attribute.
|
||||
* Allow passing invokable commands to `Symfony\Component\Console\Tester\CommandTester`
|
||||
* Add `#[MapInput]` attribute to support DTOs in commands
|
||||
* Add optional timeout for interaction in `QuestionHelper`
|
||||
* Add support for interactive invokable commands with `#[Interact]` and `#[Ask]` attributes
|
||||
* Add support for `Cursor` helper in invokable commands
|
||||
|
||||
7.3
|
||||
---
|
||||
|
||||
* Add `TreeHelper` and `TreeStyle` to display tree-like structures
|
||||
* Add `SymfonyStyle::createTree()`
|
||||
* Add support for invokable commands and add `#[Argument]` and `#[Option]` attributes to define input arguments and options
|
||||
* Deprecate not declaring the parameter type in callable commands defined through `setCode` method
|
||||
* Add support for help definition via `AsCommand` attribute
|
||||
* Deprecate methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute
|
||||
* Add support for Markdown format in `Table`
|
||||
* Add support for `LockableTrait` in invokable commands
|
||||
* Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()`
|
||||
* Mark `#[AsCommand]` attribute as `@final`
|
||||
* Add support for `SignalableCommandInterface` with invokable commands
|
||||
|
||||
7.2
|
||||
---
|
||||
|
||||
* Add support for `FORCE_COLOR` environment variable
|
||||
* Add `verbosity` argument to `mustRun` process helper method
|
||||
* [BC BREAK] Add silent verbosity (`--silent`/`SHELL_VERBOSITY=-2`) to suppress all output, including errors
|
||||
* Add `OutputInterface::isSilent()`, `Output::isSilent()`, `OutputStyle::isSilent()` methods
|
||||
* Add a configurable finished indicator to the progress indicator to show that the progress is finished
|
||||
* Add ability to schedule alarm signals and a `ConsoleAlarmEvent`
|
||||
|
||||
7.1
|
||||
---
|
||||
|
||||
* Add `ArgvInput::getRawTokens()`
|
||||
|
||||
7.0
|
||||
---
|
||||
|
||||
* Add method `__toString()` to `InputInterface`
|
||||
* Remove `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
|
||||
* Require explicit argument when calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()` and `Question::setAutocompleterCallback/setValidator()`
|
||||
* Remove `StringInput::REGEX_STRING`
|
||||
|
||||
6.4
|
||||
---
|
||||
|
||||
* Add `SignalMap` to map signal value to its name
|
||||
* Multi-line text in vertical tables is aligned properly
|
||||
* The application can also catch errors with `Application::setCatchErrors(true)`
|
||||
* Add `RunCommandMessage` and `RunCommandMessageHandler`
|
||||
* Dispatch `ConsoleTerminateEvent` after an exit on signal handling and add `ConsoleTerminateEvent::getInterruptingSignal()`
|
||||
|
||||
6.3
|
||||
---
|
||||
|
||||
* Add support for choosing exit code while handling signal, or to not exit at all
|
||||
* Add `ProgressBar::setPlaceholderFormatter` to set a placeholder attached to a instance, instead of being global.
|
||||
* Add `ReStructuredTextDescriptor`
|
||||
|
||||
6.2
|
||||
---
|
||||
|
||||
* Improve truecolor terminal detection in some cases
|
||||
* Add support for 256 color terminals (conversion from Ansi24 to Ansi8 if terminal is capable of it)
|
||||
* Deprecate calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()`, `Question::setAutocompleterCallback/setValidator()`without any arguments
|
||||
* Change the signature of `OutputFormatterStyleInterface::setForeground/setBackground()` to `setForeground/setBackground(?string)`
|
||||
* Change the signature of `HelperInterface::setHelperSet()` to `setHelperSet(?HelperSet)`
|
||||
|
||||
6.1
|
||||
---
|
||||
|
||||
* Add support to display table vertically when calling setVertical()
|
||||
* Add method `__toString()` to `InputInterface`
|
||||
* Added `OutputWrapper` to prevent truncated URL in `SymfonyStyle::createBlock`.
|
||||
* Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
|
||||
* Add suggested values for arguments and options in input definition, for input completion
|
||||
* Add `$resumeAt` parameter to `ProgressBar#start()`, so that one can easily 'resume' progress on longer tasks, and still get accurate `getEstimate()` and `getRemaining()` results.
|
||||
|
||||
6.0
|
||||
---
|
||||
|
||||
* `Command::setHidden()` has a default value (`true`) for `$hidden` parameter and is final
|
||||
* Remove `Helper::strlen()`, use `Helper::width()` instead
|
||||
* Remove `Helper::strlenWithoutDecoration()`, use `Helper::removeDecoration()` instead
|
||||
* `AddConsoleCommandPass` can not be configured anymore
|
||||
* Remove `HelperSet::setCommand()` and `getCommand()` without replacement
|
||||
|
||||
5.4
|
||||
---
|
||||
|
||||
* Add `TesterTrait::assertCommandIsSuccessful()` to test command
|
||||
* Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add `GithubActionReporter` to render annotations in a Github Action
|
||||
* Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options
|
||||
* Add the `Command::$defaultDescription` static property and the `description` attribute
|
||||
on the `console.command` tag to allow the `list` command to instantiate commands lazily
|
||||
* Add option `--short` to the `list` command
|
||||
* Add support for bright colors
|
||||
* Add `#[AsCommand]` attribute for declaring commands on PHP 8
|
||||
* Add `Helper::width()` and `Helper::length()`
|
||||
* The `--ansi` and `--no-ansi` options now default to `null`.
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester`
|
||||
* added support for multiline responses to questions through `Question::setMultiline()`
|
||||
and `Question::isMultiline()`
|
||||
* Added `SignalRegistry` class to stack signals handlers
|
||||
* Added support for signals:
|
||||
* Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods
|
||||
* Added `SignalableCommandInterface` interface
|
||||
* Added `TableCellStyle` class to customize table cell
|
||||
* Removed `php ` prefix invocation from help messages.
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* `Command::setHidden()` is final since Symfony 5.1
|
||||
* Add `SingleCommandApplication`
|
||||
* Add `Cursor` class
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
* removed support for finding hidden commands using an abbreviation, use the full name instead
|
||||
* removed `TableStyle::setCrossingChar()` method in favor of `TableStyle::setDefaultCrossingChar()`
|
||||
* removed `TableStyle::setHorizontalBorderChar()` method in favor of `TableStyle::setDefaultCrossingChars()`
|
||||
* removed `TableStyle::getHorizontalBorderChar()` method in favor of `TableStyle::getBorderChars()`
|
||||
* removed `TableStyle::setVerticalBorderChar()` method in favor of `TableStyle::setVerticalBorderChars()`
|
||||
* removed `TableStyle::getVerticalBorderChar()` method in favor of `TableStyle::getBorderChars()`
|
||||
* removed support for returning `null` from `Command::execute()`, return `0` instead
|
||||
* `ProcessHelper::run()` accepts only `array|Symfony\Component\Process\Process` for its `command` argument
|
||||
* `Application::setDispatcher` accepts only `Symfony\Contracts\EventDispatcher\EventDispatcherInterface`
|
||||
for its `dispatcher` argument
|
||||
* renamed `Application::renderException()` and `Application::doRenderException()`
|
||||
to `renderThrowable()` and `doRenderThrowable()` respectively.
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* deprecated finding hidden commands using an abbreviation, use the full name instead
|
||||
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
|
||||
* added method `minSecondsBetweenRedraws()` and `maxSecondsBetweenRedraws()` on `ProgressBar`
|
||||
* `Application` implements `ResetInterface`
|
||||
* marked all dispatched event classes as `@final`
|
||||
* added support for displaying table horizontally
|
||||
* deprecated returning `null` from `Command::execute()`, return `0` instead
|
||||
* Deprecated the `Application::renderException()` and `Application::doRenderException()` methods,
|
||||
use `renderThrowable()` and `doRenderThrowable()` instead.
|
||||
* added support for the `NO_COLOR` env var (https://no-color.org/)
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* added support for hyperlinks
|
||||
* added `ProgressBar::iterate()` method that simplify updating the progress bar when iterating
|
||||
* added `Question::setAutocompleterCallback()` to provide a callback function
|
||||
that dynamically generates suggestions as the user types
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* allowed passing commands as `[$process, 'ENV_VAR' => 'value']` to
|
||||
`ProcessHelper::run()` to pass environment variables
|
||||
* deprecated passing a command as a string to `ProcessHelper::run()`,
|
||||
pass it the command as an array of its arguments instead
|
||||
* made the `ProcessHelper` class final
|
||||
* added `WrappableOutputFormatterInterface::formatAndWrap()` (implemented in `OutputFormatter`)
|
||||
* added `capture_stderr_separately` option to `CommandTester::execute()`
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
||||
* added option to run suggested command if command is not found and only 1 alternative is available
|
||||
* added option to modify console output and print multiple modifiable sections
|
||||
* added support for iterable messages in output `write` and `writeln` methods
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
||||
* `OutputFormatter` throws an exception when unknown options are used
|
||||
* removed `QuestionHelper::setInputStream()/getInputStream()`
|
||||
* removed `Application::getTerminalWidth()/getTerminalHeight()` and
|
||||
`Application::setTerminalDimensions()/getTerminalDimensions()`
|
||||
* removed `ConsoleExceptionEvent`
|
||||
* removed `ConsoleEvents::EXCEPTION`
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* added `SHELL_VERBOSITY` env var to control verbosity
|
||||
* added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11
|
||||
`ContainerCommandLoader` for commands lazy-loading
|
||||
* added a case-insensitive command name matching fallback
|
||||
* added static `Command::$defaultName/getDefaultName()`, allowing for
|
||||
commands to be registered at compile time in the application command loader.
|
||||
Setting the `$defaultName` property avoids the need for filling the `command`
|
||||
attribute on the `console.command` tag when using `AddConsoleCommandPass`.
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
* added `ExceptionListener`
|
||||
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
|
||||
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
|
||||
with value optional explicitly passed empty
|
||||
* added console.error event to catch exceptions thrown by other listeners
|
||||
* deprecated console.exception event in favor of console.error
|
||||
* added ability to handle `CommandNotFoundException` through the
|
||||
`console.error` event
|
||||
* deprecated default validation in `SymfonyQuestionHelper::ask`
|
||||
|
||||
3.2.0
|
||||
------
|
||||
|
||||
* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
|
||||
* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
|
||||
* added StreamableInputInterface
|
||||
* added LockableTrait
|
||||
|
||||
3.1.0
|
||||
-----
|
||||
|
||||
* added truncate method to FormatterHelper
|
||||
* added setColumnWidth(s) method to Table
|
||||
|
||||
2.8.3
|
||||
-----
|
||||
|
||||
* remove readline support from the question helper as it caused issues
|
||||
|
||||
2.8.0
|
||||
-----
|
||||
|
||||
* use readline for user input in the question helper when available to allow
|
||||
the use of arrow keys
|
||||
|
||||
2.6.0
|
||||
-----
|
||||
|
||||
* added a Process helper
|
||||
* added a DebugFormatter helper
|
||||
|
||||
2.5.0
|
||||
-----
|
||||
|
||||
* deprecated the dialog helper (use the question helper instead)
|
||||
* deprecated TableHelper in favor of Table
|
||||
* deprecated ProgressHelper in favor of ProgressBar
|
||||
* added ConsoleLogger
|
||||
* added a question helper
|
||||
* added a way to set the process name of a command
|
||||
* added a way to set a default command instead of `ListCommand`
|
||||
|
||||
2.4.0
|
||||
-----
|
||||
|
||||
* added a way to force terminal dimensions
|
||||
* added a convenient method to detect verbosity level
|
||||
* [BC BREAK] made descriptors use output instead of returning a string
|
||||
|
||||
2.3.0
|
||||
-----
|
||||
|
||||
* added multiselect support to the select dialog helper
|
||||
* added Table Helper for tabular data rendering
|
||||
* added support for events in `Application`
|
||||
* added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()`
|
||||
* added a way to set the progress bar progress via the `setCurrent` method
|
||||
* added support for multiple InputOption shortcuts, written as `'-a|-b|-c'`
|
||||
* added two additional verbosity levels, VERBOSITY_VERY_VERBOSE and VERBOSITY_DEBUG
|
||||
|
||||
2.2.0
|
||||
-----
|
||||
|
||||
* added support for colorization on Windows via ConEmu
|
||||
* add a method to Dialog Helper to ask for a question and hide the response
|
||||
* added support for interactive selections in console (DialogHelper::select())
|
||||
* added support for autocompletion as you type in Dialog Helper
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
||||
* added ConsoleOutputInterface
|
||||
* added the possibility to disable a command (Command::isEnabled())
|
||||
* added suggestions when a command does not exist
|
||||
* added a --raw option to the list command
|
||||
* added support for STDERR in the console output class (errors are now sent
|
||||
to STDERR)
|
||||
* made the defaults (helper set, commands, input definition) in Application
|
||||
more easily customizable
|
||||
* added support for the shell even if readline is not available
|
||||
* added support for process isolation in Symfony shell via
|
||||
`--process-isolation` switch
|
||||
* added support for `--`, which disables options parsing after that point
|
||||
(tokens will be parsed as arguments)
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\CI;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Utility class for Github actions.
|
||||
*
|
||||
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
|
||||
*/
|
||||
class GithubActionReporter
|
||||
{
|
||||
/**
|
||||
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
|
||||
*/
|
||||
private const ESCAPED_DATA = [
|
||||
'%' => '%25',
|
||||
"\r" => '%0D',
|
||||
"\n" => '%0A',
|
||||
];
|
||||
|
||||
/**
|
||||
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94
|
||||
*/
|
||||
private const ESCAPED_PROPERTIES = [
|
||||
'%' => '%25',
|
||||
"\r" => '%0D',
|
||||
"\n" => '%0A',
|
||||
':' => '%3A',
|
||||
',' => '%2C',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private OutputInterface $output,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function isGithubActionEnvironment(): bool
|
||||
{
|
||||
return false !== getenv('GITHUB_ACTIONS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Output an error using the Github annotations format.
|
||||
*
|
||||
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
||||
*/
|
||||
public function error(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
|
||||
{
|
||||
$this->log('error', $message, $file, $line, $col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a warning using the Github annotations format.
|
||||
*
|
||||
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
|
||||
*/
|
||||
public function warning(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
|
||||
{
|
||||
$this->log('warning', $message, $file, $line, $col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a debug log using the Github annotations format.
|
||||
*
|
||||
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
|
||||
*/
|
||||
public function debug(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
|
||||
{
|
||||
$this->log('debug', $message, $file, $line, $col);
|
||||
}
|
||||
|
||||
private function log(string $type, string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
|
||||
{
|
||||
// Some values must be encoded.
|
||||
$message = strtr($message, self::ESCAPED_DATA);
|
||||
|
||||
if (!$file) {
|
||||
// No file provided, output the message solely:
|
||||
$this->output->writeln(\sprintf('::%s::%s', $type, $message));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln(\sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
|
||||
}
|
||||
}
|
||||
Vendored
+133
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console;
|
||||
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class Color
|
||||
{
|
||||
private const COLORS = [
|
||||
'black' => 0,
|
||||
'red' => 1,
|
||||
'green' => 2,
|
||||
'yellow' => 3,
|
||||
'blue' => 4,
|
||||
'magenta' => 5,
|
||||
'cyan' => 6,
|
||||
'white' => 7,
|
||||
'default' => 9,
|
||||
];
|
||||
|
||||
private const BRIGHT_COLORS = [
|
||||
'gray' => 0,
|
||||
'bright-red' => 1,
|
||||
'bright-green' => 2,
|
||||
'bright-yellow' => 3,
|
||||
'bright-blue' => 4,
|
||||
'bright-magenta' => 5,
|
||||
'bright-cyan' => 6,
|
||||
'bright-white' => 7,
|
||||
];
|
||||
|
||||
private const AVAILABLE_OPTIONS = [
|
||||
'bold' => ['set' => 1, 'unset' => 22],
|
||||
'underscore' => ['set' => 4, 'unset' => 24],
|
||||
'blink' => ['set' => 5, 'unset' => 25],
|
||||
'reverse' => ['set' => 7, 'unset' => 27],
|
||||
'conceal' => ['set' => 8, 'unset' => 28],
|
||||
];
|
||||
|
||||
private string $foreground;
|
||||
private string $background;
|
||||
private array $options = [];
|
||||
|
||||
public function __construct(string $foreground = '', string $background = '', array $options = [])
|
||||
{
|
||||
$this->foreground = $this->parseColor($foreground);
|
||||
$this->background = $this->parseColor($background, true);
|
||||
|
||||
foreach ($options as $option) {
|
||||
if (!isset(self::AVAILABLE_OPTIONS[$option])) {
|
||||
throw new InvalidArgumentException(\sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
|
||||
}
|
||||
|
||||
$this->options[$option] = self::AVAILABLE_OPTIONS[$option];
|
||||
}
|
||||
}
|
||||
|
||||
public function apply(string $text): string
|
||||
{
|
||||
return $this->set().$text.$this->unset();
|
||||
}
|
||||
|
||||
public function set(): string
|
||||
{
|
||||
$setCodes = [];
|
||||
if ('' !== $this->foreground) {
|
||||
$setCodes[] = $this->foreground;
|
||||
}
|
||||
if ('' !== $this->background) {
|
||||
$setCodes[] = $this->background;
|
||||
}
|
||||
foreach ($this->options as $option) {
|
||||
$setCodes[] = $option['set'];
|
||||
}
|
||||
if (0 === \count($setCodes)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \sprintf("\033[%sm", implode(';', $setCodes));
|
||||
}
|
||||
|
||||
public function unset(): string
|
||||
{
|
||||
$unsetCodes = [];
|
||||
if ('' !== $this->foreground) {
|
||||
$unsetCodes[] = 39;
|
||||
}
|
||||
if ('' !== $this->background) {
|
||||
$unsetCodes[] = 49;
|
||||
}
|
||||
foreach ($this->options as $option) {
|
||||
$unsetCodes[] = $option['unset'];
|
||||
}
|
||||
if (0 === \count($unsetCodes)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \sprintf("\033[%sm", implode(';', $unsetCodes));
|
||||
}
|
||||
|
||||
private function parseColor(string $color, bool $background = false): string
|
||||
{
|
||||
if ('' === $color) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ('#' === $color[0]) {
|
||||
return ($background ? '4' : '3').Terminal::getColorMode()->convertFromHexToAnsiColorCode($color);
|
||||
}
|
||||
|
||||
if (isset(self::COLORS[$color])) {
|
||||
return ($background ? '4' : '3').self::COLORS[$color];
|
||||
}
|
||||
|
||||
if (isset(self::BRIGHT_COLORS[$color])) {
|
||||
return ($background ? '10' : '9').self::BRIGHT_COLORS[$color];
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(\sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
|
||||
}
|
||||
}
|
||||
+706
@@ -0,0 +1,706 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Helper\HelperInterface;
|
||||
use Symfony\Component\Console\Helper\HelperSet;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Base class for all commands.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class Command implements SignalableCommandInterface
|
||||
{
|
||||
// see https://tldp.org/LDP/abs/html/exitcodes.html
|
||||
public const SUCCESS = 0;
|
||||
public const FAILURE = 1;
|
||||
public const INVALID = 2;
|
||||
|
||||
private ?Application $application = null;
|
||||
private ?string $name = null;
|
||||
private ?string $processTitle = null;
|
||||
private array $aliases = [];
|
||||
private InputDefinition $definition;
|
||||
private bool $hidden = false;
|
||||
private string $help = '';
|
||||
private string $description = '';
|
||||
private ?InputDefinition $fullDefinition = null;
|
||||
private bool $ignoreValidationErrors = false;
|
||||
private ?InvokableCommand $code = null;
|
||||
private array $synopsis = [];
|
||||
private array $usages = [];
|
||||
private ?HelperSet $helperSet = null;
|
||||
|
||||
/**
|
||||
* @param string|null $name The name of the command; passing null means it must be set in configure()
|
||||
*
|
||||
* @throws LogicException When the command name is empty
|
||||
*/
|
||||
public function __construct(?string $name = null, ?callable $code = null)
|
||||
{
|
||||
$this->definition = new InputDefinition();
|
||||
|
||||
$attribute = $this->getCommandAttribute($code);
|
||||
|
||||
if ($code) {
|
||||
$this->setCode($code);
|
||||
}
|
||||
|
||||
if (null !== $name ??= $attribute?->name) {
|
||||
$aliases = explode('|', $name);
|
||||
|
||||
if ('' === $name = array_shift($aliases)) {
|
||||
$this->setHidden(true);
|
||||
$name = array_shift($aliases);
|
||||
}
|
||||
|
||||
// we must not overwrite existing aliases, combine new ones with existing ones
|
||||
$aliases = array_unique([
|
||||
...$this->aliases,
|
||||
...$aliases,
|
||||
]);
|
||||
|
||||
$this->setAliases($aliases);
|
||||
}
|
||||
|
||||
if (null !== $name) {
|
||||
$this->setName($name);
|
||||
}
|
||||
|
||||
if ('' === $this->description) {
|
||||
$this->setDescription($attribute?->description ?? '');
|
||||
}
|
||||
|
||||
if ('' === $this->help) {
|
||||
$this->setHelp($attribute?->help ?? '');
|
||||
}
|
||||
|
||||
foreach ($attribute?->usages ?? [] as $usage) {
|
||||
$this->addUsage($usage);
|
||||
}
|
||||
|
||||
if (!$code && \is_callable($this) && self::class === (new \ReflectionMethod($this, 'execute'))->class) {
|
||||
$this->code = new InvokableCommand($this, $this(...));
|
||||
}
|
||||
|
||||
$this->configure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignores validation errors.
|
||||
*
|
||||
* This is mainly useful for the help command.
|
||||
*/
|
||||
public function ignoreValidationErrors(): void
|
||||
{
|
||||
$this->ignoreValidationErrors = true;
|
||||
}
|
||||
|
||||
public function setApplication(?Application $application): void
|
||||
{
|
||||
$this->application = $application;
|
||||
if ($application) {
|
||||
$this->setHelperSet($application->getHelperSet());
|
||||
} else {
|
||||
$this->helperSet = null;
|
||||
}
|
||||
|
||||
$this->fullDefinition = null;
|
||||
}
|
||||
|
||||
public function setHelperSet(HelperSet $helperSet): void
|
||||
{
|
||||
$this->helperSet = $helperSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the helper set.
|
||||
*/
|
||||
public function getHelperSet(): ?HelperSet
|
||||
{
|
||||
return $this->helperSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application instance for this command.
|
||||
*/
|
||||
public function getApplication(): ?Application
|
||||
{
|
||||
return $this->application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the command is enabled or not in the current environment.
|
||||
*
|
||||
* Override this to check for x or y and return false if the command cannot
|
||||
* run properly under the current conditions.
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the current command.
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the current command.
|
||||
*
|
||||
* This method is not abstract because you can use this class
|
||||
* as a concrete class. In this case, instead of defining the
|
||||
* execute() method, you set the code to execute by passing
|
||||
* a Closure to the setCode() method.
|
||||
*
|
||||
* @return int 0 if everything went fine, or an exit code
|
||||
*
|
||||
* @throws LogicException When this abstract method is not implemented
|
||||
*
|
||||
* @see setCode()
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
throw new LogicException('You must override the execute() method in the concrete command class.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Interacts with the user.
|
||||
*
|
||||
* This method is executed before the InputDefinition is validated.
|
||||
* This means that this is the only place where the command can
|
||||
* interactively ask for values of missing required arguments.
|
||||
*/
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the command after the input has been bound and before the input
|
||||
* is validated.
|
||||
*
|
||||
* This is mainly useful when a lot of commands extends one main command
|
||||
* where some things need to be initialized based on the input arguments and options.
|
||||
*
|
||||
* @see InputInterface::bind()
|
||||
* @see InputInterface::validate()
|
||||
*/
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the command.
|
||||
*
|
||||
* The code to execute is either defined directly with the
|
||||
* setCode() method or by overriding the execute() method
|
||||
* in a sub-class.
|
||||
*
|
||||
* @return int The command exit code
|
||||
*
|
||||
* @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}.
|
||||
*
|
||||
* @see setCode()
|
||||
* @see execute()
|
||||
*/
|
||||
public function run(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// add the application arguments and options
|
||||
$this->mergeApplicationDefinition();
|
||||
|
||||
// bind the input against the command specific arguments/options
|
||||
try {
|
||||
$input->bind($this->getDefinition());
|
||||
} catch (ExceptionInterface $e) {
|
||||
if (!$this->ignoreValidationErrors) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$this->initialize($input, $output);
|
||||
|
||||
if (null !== $this->processTitle) {
|
||||
if (\function_exists('cli_set_process_title')) {
|
||||
if (!@cli_set_process_title($this->processTitle)) {
|
||||
if ('Darwin' === \PHP_OS) {
|
||||
$output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
|
||||
} else {
|
||||
cli_set_process_title($this->processTitle);
|
||||
}
|
||||
}
|
||||
} elseif (\function_exists('setproctitle')) {
|
||||
setproctitle($this->processTitle);
|
||||
} elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
|
||||
$output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
|
||||
}
|
||||
}
|
||||
|
||||
if ($input->isInteractive()) {
|
||||
$this->interact($input, $output);
|
||||
|
||||
if ($this->code?->isInteractive()) {
|
||||
$this->code->interact($input, $output);
|
||||
}
|
||||
}
|
||||
|
||||
// The command name argument is often omitted when a command is executed directly with its run() method.
|
||||
// It would fail the validation if we didn't make sure the command argument is present,
|
||||
// since it's required by the application.
|
||||
if ($input->hasArgument('command') && null === $input->getArgument('command')) {
|
||||
$input->setArgument('command', $this->getName());
|
||||
}
|
||||
|
||||
$input->validate();
|
||||
|
||||
if ($this->code) {
|
||||
return ($this->code)($input, $output);
|
||||
}
|
||||
|
||||
return $this->execute($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplies suggestions when resolving possible completion options for input (e.g. option or argument).
|
||||
*/
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
$definition = $this->getDefinition();
|
||||
if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) {
|
||||
$definition->getOption($input->getCompletionName())->complete($input, $suggestions);
|
||||
} elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
|
||||
$definition->getArgument($input->getCompletionName())->complete($input, $suggestions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the code that is executed by the command.
|
||||
*
|
||||
* @return ?callable null if the code has not been set with setCode()
|
||||
*/
|
||||
public function getCode(): ?callable
|
||||
{
|
||||
return $this->code?->getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the code to execute when running this command.
|
||||
*
|
||||
* If this method is used, it overrides the code defined
|
||||
* in the execute() method.
|
||||
*
|
||||
* @param callable $code A callable(InputInterface $input, OutputInterface $output)
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @see execute()
|
||||
*/
|
||||
public function setCode(callable $code): static
|
||||
{
|
||||
$this->code = new InvokableCommand($this, $code);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the application definition with the command definition.
|
||||
*
|
||||
* This method is not part of public API and should not be used directly.
|
||||
*
|
||||
* @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function mergeApplicationDefinition(bool $mergeArgs = true): void
|
||||
{
|
||||
if (null === $this->application) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fullDefinition = new InputDefinition();
|
||||
$this->fullDefinition->setOptions($this->definition->getOptions());
|
||||
$this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
|
||||
|
||||
if ($mergeArgs) {
|
||||
$this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
|
||||
$this->fullDefinition->addArguments($this->definition->getArguments());
|
||||
} else {
|
||||
$this->fullDefinition->setArguments($this->definition->getArguments());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an array of argument and option instances.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDefinition(array|InputDefinition $definition): static
|
||||
{
|
||||
if ($definition instanceof InputDefinition) {
|
||||
$this->definition = $definition;
|
||||
} else {
|
||||
$this->definition->setDefinition($definition);
|
||||
}
|
||||
|
||||
$this->fullDefinition = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the InputDefinition attached to this Command.
|
||||
*/
|
||||
public function getDefinition(): InputDefinition
|
||||
{
|
||||
return $this->fullDefinition ?? $this->getNativeDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the InputDefinition to be used to create representations of this Command.
|
||||
*
|
||||
* Can be overridden to provide the original command representation when it would otherwise
|
||||
* be changed by merging with the application InputDefinition.
|
||||
*
|
||||
* This method is not part of public API and should not be used directly.
|
||||
*/
|
||||
public function getNativeDefinition(): InputDefinition
|
||||
{
|
||||
$definition = $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
|
||||
|
||||
if ($this->code && !$definition->getArguments() && !$definition->getOptions()) {
|
||||
$this->code->configure($definition);
|
||||
}
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an argument.
|
||||
*
|
||||
* @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
|
||||
* @param $default The default value (for InputArgument::OPTIONAL mode only)
|
||||
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException When argument mode is not valid
|
||||
*/
|
||||
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
|
||||
{
|
||||
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
|
||||
$this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an option.
|
||||
*
|
||||
* @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
||||
* @param $mode The option mode: One of the InputOption::VALUE_* constants
|
||||
* @param $default The default value (must be null for InputOption::VALUE_NONE)
|
||||
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If option mode is invalid or incompatible
|
||||
*/
|
||||
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
|
||||
{
|
||||
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
|
||||
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the command.
|
||||
*
|
||||
* This method can set both the namespace and the name if
|
||||
* you separate them by a colon (:)
|
||||
*
|
||||
* $command->setName('foo:bar');
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException When the name is invalid
|
||||
*/
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->validateName($name);
|
||||
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the process title of the command.
|
||||
*
|
||||
* This feature should be used only when creating a long process command,
|
||||
* like a daemon.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setProcessTitle(string $title): static
|
||||
{
|
||||
$this->processTitle = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the command name.
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $hidden Whether or not the command should be hidden from the list of commands
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setHidden(bool $hidden = true): static
|
||||
{
|
||||
$this->hidden = $hidden;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool whether the command should be publicly shown or not
|
||||
*/
|
||||
public function isHidden(): bool
|
||||
{
|
||||
return $this->hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description for the command.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDescription(string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description for the command.
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the help for the command.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setHelp(string $help): static
|
||||
{
|
||||
$this->help = $help;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the help for the command.
|
||||
*/
|
||||
public function getHelp(): string
|
||||
{
|
||||
return $this->help;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the processed help for the command replacing the %command.name% and
|
||||
* %command.full_name% patterns with the real values dynamically.
|
||||
*/
|
||||
public function getProcessedHelp(): string
|
||||
{
|
||||
$name = $this->name;
|
||||
$isSingleCommand = $this->application?->isSingleCommand();
|
||||
|
||||
$placeholders = [
|
||||
'%command.name%',
|
||||
'%command.full_name%',
|
||||
];
|
||||
$replacements = [
|
||||
$name,
|
||||
$isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
|
||||
];
|
||||
|
||||
return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the aliases for the command.
|
||||
*
|
||||
* @param string[] $aliases An array of aliases for the command
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException When an alias is invalid
|
||||
*/
|
||||
public function setAliases(iterable $aliases): static
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($aliases as $alias) {
|
||||
$this->validateName($alias);
|
||||
$list[] = $alias;
|
||||
}
|
||||
|
||||
$this->aliases = $list;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the aliases for the command.
|
||||
*/
|
||||
public function getAliases(): array
|
||||
{
|
||||
return $this->aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the synopsis for the command.
|
||||
*
|
||||
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
|
||||
*/
|
||||
public function getSynopsis(bool $short = false): string
|
||||
{
|
||||
$key = $short ? 'short' : 'long';
|
||||
|
||||
if (!isset($this->synopsis[$key])) {
|
||||
$this->synopsis[$key] = trim(\sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
|
||||
}
|
||||
|
||||
return $this->synopsis[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a command usage example, it'll be prefixed with the command name.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addUsage(string $usage): static
|
||||
{
|
||||
if (!str_starts_with($usage, $this->name)) {
|
||||
$usage = \sprintf('%s %s', $this->name, $usage);
|
||||
}
|
||||
|
||||
$this->usages[] = $usage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns alternative usages of the command.
|
||||
*/
|
||||
public function getUsages(): array
|
||||
{
|
||||
return $this->usages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a helper instance by name.
|
||||
*
|
||||
* @throws LogicException if no HelperSet is defined
|
||||
* @throws InvalidArgumentException if the helper is not defined
|
||||
*/
|
||||
public function getHelper(string $name): HelperInterface
|
||||
{
|
||||
if (null === $this->helperSet) {
|
||||
throw new LogicException(\sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
|
||||
}
|
||||
|
||||
return $this->helperSet->get($name);
|
||||
}
|
||||
|
||||
public function getSubscribedSignals(): array
|
||||
{
|
||||
return $this->code?->getSubscribedSignals() ?? [];
|
||||
}
|
||||
|
||||
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
|
||||
{
|
||||
return $this->code?->handleSignal($signal, $previousExitCode) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a command name.
|
||||
*
|
||||
* It must be non-empty and parts can optionally be separated by ":".
|
||||
*
|
||||
* @throws InvalidArgumentException When the name is invalid
|
||||
*/
|
||||
private function validateName(string $name): void
|
||||
{
|
||||
if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
|
||||
throw new InvalidArgumentException(\sprintf('Command name "%s" is invalid.', $name));
|
||||
}
|
||||
}
|
||||
|
||||
private function getCommandAttribute(?callable $code): ?AsCommand
|
||||
{
|
||||
if (null === $code) {
|
||||
/** @var AsCommand|null $attribute */
|
||||
$attribute = (new \ReflectionClass(static::class)->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
|
||||
|
||||
return $attribute;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionFunction($code(...));
|
||||
|
||||
if ($reflection->isAnonymous() || !$class = $reflection->getClosureScopeClass()) {
|
||||
throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s", an invokable object or a method of an object.', self::class));
|
||||
}
|
||||
|
||||
/** @var AsCommand|null $attribute */
|
||||
$attribute = ($reflection->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
|
||||
|
||||
if (!$attribute && '__invoke' === $reflection->getName()) {
|
||||
/** @var AsCommand|null $attribute */
|
||||
$attribute = ($class->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
|
||||
}
|
||||
|
||||
if (!$attribute) {
|
||||
throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class));
|
||||
}
|
||||
|
||||
return $attribute;
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
|
||||
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
|
||||
use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
|
||||
use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Responsible for providing the values to the shell completion.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
|
||||
final class CompleteCommand extends Command
|
||||
{
|
||||
public const COMPLETION_API_VERSION = '1';
|
||||
|
||||
private array $completionOutputs;
|
||||
private bool $isDebug = false;
|
||||
|
||||
/**
|
||||
* @param array<string, class-string<CompletionOutputInterface>> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value
|
||||
*/
|
||||
public function __construct(array $completionOutputs = [])
|
||||
{
|
||||
// must be set before the parent constructor, as the property value is used in configure()
|
||||
$this->completionOutputs = $completionOutputs + [
|
||||
'bash' => BashCompletionOutput::class,
|
||||
'fish' => FishCompletionOutput::class,
|
||||
'zsh' => ZshCompletionOutput::class,
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")')
|
||||
->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)')
|
||||
->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)')
|
||||
->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script')
|
||||
->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated')
|
||||
;
|
||||
}
|
||||
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
try {
|
||||
// "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1
|
||||
$version = $input->getOption('symfony') ? '1' : $input->getOption('api-version');
|
||||
if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) {
|
||||
$message = \sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION);
|
||||
$this->log($message);
|
||||
|
||||
$output->writeln($message.' Install the Symfony completion script again by using the "completion" command.');
|
||||
|
||||
return 126;
|
||||
}
|
||||
|
||||
$shell = $input->getOption('shell');
|
||||
if (!$shell) {
|
||||
throw new \RuntimeException('The "--shell" option must be set.');
|
||||
}
|
||||
|
||||
if (!$completionOutput = $this->completionOutputs[$shell] ?? false) {
|
||||
throw new \RuntimeException(\sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
|
||||
}
|
||||
|
||||
$completionInput = $this->createCompletionInput($input);
|
||||
$suggestions = new CompletionSuggestions();
|
||||
|
||||
$this->log([
|
||||
'',
|
||||
'<comment>'.date('Y-m-d H:i:s').'</>',
|
||||
'<info>Input:</> <comment>("|" indicates the cursor position)</>',
|
||||
' '.$completionInput,
|
||||
'<info>Command:</>',
|
||||
' '.implode(' ', $_SERVER['argv']),
|
||||
'<info>Messages:</>',
|
||||
]);
|
||||
|
||||
if ($command = $this->findCommand($completionInput)) {
|
||||
$command->mergeApplicationDefinition();
|
||||
$completionInput->bind($command->getDefinition());
|
||||
}
|
||||
if (null === $command) {
|
||||
$this->log(' No command found, completing using the Application class.');
|
||||
|
||||
$this->getApplication()->complete($completionInput, $suggestions);
|
||||
} elseif (
|
||||
$completionInput->mustSuggestArgumentValuesFor('command')
|
||||
) {
|
||||
$this->log(' Command found, completing command name.');
|
||||
|
||||
// expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
|
||||
$commandNames = array_filter(array_merge([$command->getName()], $command->getAliases()));
|
||||
foreach ($commandNames as $name) {
|
||||
if (str_starts_with($name, $completionInput->getCompletionValue())) {
|
||||
$commandNames = [$name];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$suggestions->suggestValues($commandNames);
|
||||
} else {
|
||||
if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
|
||||
$this->log(' Completing option names for the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> command.');
|
||||
|
||||
$suggestions->suggestOptions($command->getDefinition()->getOptions());
|
||||
} else {
|
||||
$this->log([
|
||||
' Completing using the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> class.',
|
||||
' Completing <comment>'.$completionInput->getCompletionType().'</> for <comment>'.$completionInput->getCompletionName().'</>',
|
||||
]);
|
||||
if (null !== $compval = $completionInput->getCompletionValue()) {
|
||||
$this->log(' Current value: <comment>'.$compval.'</>');
|
||||
}
|
||||
|
||||
$command->complete($completionInput, $suggestions);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var CompletionOutputInterface $completionOutput */
|
||||
$completionOutput = new $completionOutput();
|
||||
|
||||
$this->log('<info>Suggestions:</>');
|
||||
if ($options = $suggestions->getOptionSuggestions()) {
|
||||
$this->log(' --'.implode(' --', array_map(static fn ($o) => $o->getName(), $options)));
|
||||
} elseif ($values = $suggestions->getValueSuggestions()) {
|
||||
$this->log(' '.implode(' ', $values));
|
||||
} else {
|
||||
$this->log(' <comment>No suggestions were provided</>');
|
||||
}
|
||||
|
||||
$completionOutput->write($suggestions, $output);
|
||||
} catch (\Throwable $e) {
|
||||
$this->log([
|
||||
'<error>Error!</error>',
|
||||
(string) $e,
|
||||
]);
|
||||
|
||||
if ($output->isDebug()) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function createCompletionInput(InputInterface $input): CompletionInput
|
||||
{
|
||||
$currentIndex = $input->getOption('current');
|
||||
if (!$currentIndex || !ctype_digit($currentIndex)) {
|
||||
throw new \RuntimeException('The "--current" option must be set and it must be an integer.');
|
||||
}
|
||||
|
||||
$completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex);
|
||||
|
||||
try {
|
||||
$completionInput->bind($this->getApplication()->getDefinition());
|
||||
} catch (ExceptionInterface) {
|
||||
}
|
||||
|
||||
return $completionInput;
|
||||
}
|
||||
|
||||
private function findCommand(CompletionInput $completionInput): ?Command
|
||||
{
|
||||
try {
|
||||
$inputName = $completionInput->getFirstArgument();
|
||||
if (null === $inputName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getApplication()->find($inputName);
|
||||
} catch (CommandNotFoundException) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function log($messages): void
|
||||
{
|
||||
if (!$this->isDebug) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commandName = basename($_SERVER['argv'][0]);
|
||||
file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* Dumps the completion script for the current shell.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
#[AsCommand(name: 'completion', description: 'Dump the shell completion script')]
|
||||
final class DumpCompletionCommand extends Command
|
||||
{
|
||||
private array $supportedShells;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$fullCommand = $_SERVER['PHP_SELF'];
|
||||
$commandName = basename($fullCommand);
|
||||
$fullCommand = @realpath($fullCommand) ?: $fullCommand;
|
||||
|
||||
$shell = self::guessShell();
|
||||
[$rcFile, $completionFile] = match ($shell) {
|
||||
'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"],
|
||||
'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName],
|
||||
default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"],
|
||||
};
|
||||
|
||||
$supportedShells = implode(', ', $this->getSupportedShells());
|
||||
|
||||
$this
|
||||
->setHelp(<<<EOH
|
||||
The <info>%command.name%</> command dumps the shell completion script required
|
||||
to use shell autocompletion (currently, {$supportedShells} completion are supported).
|
||||
|
||||
<comment>Static installation
|
||||
-------------------</>
|
||||
|
||||
Dump the script to a global completion file and restart your shell:
|
||||
|
||||
<info>%command.full_name% {$shell} | sudo tee {$completionFile}</>
|
||||
|
||||
Or dump the script to a local file and source it:
|
||||
|
||||
<info>%command.full_name% {$shell} > completion.sh</>
|
||||
|
||||
<comment># source the file whenever you use the project</>
|
||||
<info>source completion.sh</>
|
||||
|
||||
<comment># or add this line at the end of your "{$rcFile}" file:</>
|
||||
<info>source /path/to/completion.sh</>
|
||||
|
||||
<comment>Dynamic installation
|
||||
--------------------</>
|
||||
|
||||
Add this to the end of your shell configuration file (e.g. <info>"{$rcFile}"</>):
|
||||
|
||||
<info>eval "$({$fullCommand} completion {$shell})"</>
|
||||
EOH
|
||||
)
|
||||
->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...))
|
||||
->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$commandName = basename($_SERVER['argv'][0]);
|
||||
|
||||
if ($input->getOption('debug')) {
|
||||
$this->tailDebugLog($commandName, $output);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$shell = $input->getArgument('shell') ?? self::guessShell();
|
||||
$completionFile = __DIR__.'/../Resources/completion.'.$shell;
|
||||
if (!file_exists($completionFile)) {
|
||||
$supportedShells = $this->getSupportedShells();
|
||||
|
||||
if ($output instanceof ConsoleOutputInterface) {
|
||||
$output = $output->getErrorOutput();
|
||||
}
|
||||
if ($shell) {
|
||||
$output->writeln(\sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").</>', $shell, implode('", "', $supportedShells)));
|
||||
} else {
|
||||
$output->writeln(\sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells)));
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
$output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static function guessShell(): string
|
||||
{
|
||||
return basename($_SERVER['SHELL'] ?? '');
|
||||
}
|
||||
|
||||
private function tailDebugLog(string $commandName, OutputInterface $output): void
|
||||
{
|
||||
$debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log';
|
||||
if (!file_exists($debugFile)) {
|
||||
touch($debugFile);
|
||||
}
|
||||
$process = new Process(['tail', '-f', $debugFile], null, null, null, 0);
|
||||
$process->run(static function (string $type, string $line) use ($output): void {
|
||||
$output->write($line);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getSupportedShells(): array
|
||||
{
|
||||
if (isset($this->supportedShells)) {
|
||||
return $this->supportedShells;
|
||||
}
|
||||
|
||||
$shells = [];
|
||||
|
||||
foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) {
|
||||
if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) {
|
||||
$shells[] = $file->getExtension();
|
||||
}
|
||||
}
|
||||
sort($shells);
|
||||
|
||||
return $this->supportedShells = $shells;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Descriptor\ApplicationDescription;
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* HelpCommand displays the help for a given command.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class HelpCommand extends Command
|
||||
{
|
||||
private Command $command;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->ignoreValidationErrors();
|
||||
|
||||
$this
|
||||
->setName('help')
|
||||
->setDefinition([
|
||||
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())),
|
||||
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', static fn () => (new DescriptorHelper())->getFormats()),
|
||||
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
|
||||
])
|
||||
->setDescription('Display help for a command')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command displays help for a given command:
|
||||
|
||||
<info>%command.full_name% list</info>
|
||||
|
||||
You can also output the help in other formats by using the <info>--format</info> option:
|
||||
|
||||
<info>%command.full_name% --format=xml list</info>
|
||||
|
||||
To display the list of available commands, please use the <info>list</info> command.
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
public function setCommand(Command $command): void
|
||||
{
|
||||
$this->command = $command;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->command ??= $this->getApplication()->find($input->getArgument('command_name'));
|
||||
|
||||
$helper = new DescriptorHelper();
|
||||
$helper->describe($output, $this->command, [
|
||||
'format' => $input->getOption('format'),
|
||||
'raw_text' => $input->getOption('raw'),
|
||||
]);
|
||||
|
||||
unset($this->command);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\ArgumentResolver\ArgumentResolver;
|
||||
use Symfony\Component\Console\ArgumentResolver\ArgumentResolverInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\Interact;
|
||||
use Symfony\Component\Console\Attribute\MapInput;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
use Symfony\Component\Console\Cursor;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\RawInputInterface;
|
||||
use Symfony\Component\Console\Interaction\Interaction;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Represents an invokable command.
|
||||
*
|
||||
* @author Yonel Ceruto <open@yceruto.dev>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class InvokableCommand implements SignalableCommandInterface
|
||||
{
|
||||
private readonly ?SignalableCommandInterface $signalableCommand;
|
||||
private readonly \ReflectionFunction $invokable;
|
||||
/**
|
||||
* @var list<Interaction>|null
|
||||
*/
|
||||
private ?array $interactions = null;
|
||||
private $code;
|
||||
|
||||
public function __construct(
|
||||
private readonly Command $command,
|
||||
callable $code,
|
||||
private ?ArgumentResolverInterface $argumentResolver = null,
|
||||
) {
|
||||
$this->code = $code;
|
||||
$this->signalableCommand = $code instanceof SignalableCommandInterface ? $code : null;
|
||||
$this->invokable = new \ReflectionFunction($this->getClosure($code));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a callable with parameters generated from the input interface.
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$statusCode = $this->invokable->invoke(...$this->getParameters($this->invokable, $input, $output));
|
||||
|
||||
if (!\is_int($statusCode)) {
|
||||
throw new \TypeError(\sprintf('The command "%s" must return an integer value in the "%s" method, but "%s" was returned.', $this->command->getName(), $this->invokable->getName(), get_debug_type($statusCode)));
|
||||
}
|
||||
|
||||
return $statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the input definition from an invokable-defined function.
|
||||
*
|
||||
* Processes the parameters of the reflection function to extract and
|
||||
* add arguments or options to the provided input definition.
|
||||
*/
|
||||
public function configure(InputDefinition $definition): void
|
||||
{
|
||||
foreach ($this->invokable->getParameters() as $parameter) {
|
||||
if ($argument = Argument::tryFrom($parameter)) {
|
||||
$definition->addArgument($argument->toInputArgument());
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($option = Option::tryFrom($parameter)) {
|
||||
$definition->addOption($option->toInputOption());
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($input = MapInput::tryFrom($parameter)) {
|
||||
$inputArguments = array_map(static fn (Argument $a) => $a->toInputArgument(), iterator_to_array($input->getArguments(), false));
|
||||
|
||||
// make sure optional arguments are defined after required ones
|
||||
usort($inputArguments, static fn (InputArgument $a, InputArgument $b) => (int) $b->isRequired() - (int) $a->isRequired());
|
||||
|
||||
foreach ($inputArguments as $inputArgument) {
|
||||
$definition->addArgument($inputArgument);
|
||||
}
|
||||
|
||||
foreach ($input->getOptions() as $option) {
|
||||
$definition->addOption($option->toInputOption());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getCode(): callable
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
private function getClosure(callable $code): \Closure
|
||||
{
|
||||
if (!$code instanceof \Closure) {
|
||||
return $code(...);
|
||||
}
|
||||
|
||||
if (null !== (new \ReflectionFunction($code))->getClosureThis()) {
|
||||
return $code;
|
||||
}
|
||||
|
||||
set_error_handler(static function () {});
|
||||
try {
|
||||
if ($c = \Closure::bind($code, $this->command)) {
|
||||
$code = $c;
|
||||
}
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function getParameters(\ReflectionFunction $function, InputInterface $input, OutputInterface $output): array
|
||||
{
|
||||
$coreUtilities = [];
|
||||
$needsArgumentResolver = false;
|
||||
|
||||
foreach ($function->getParameters() as $index => $param) {
|
||||
$type = $param->getType();
|
||||
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
$argument = match ($type->getName()) {
|
||||
InputInterface::class => $input,
|
||||
RawInputInterface::class => $input,
|
||||
OutputInterface::class => $output,
|
||||
SymfonyStyle::class => new SymfonyStyle($input, $output, $this->command->getApplication()?->getDispatcher()),
|
||||
Cursor::class => new Cursor($output),
|
||||
Application::class => $this->command->getApplication(),
|
||||
Command::class, self::class => $this->command,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (null !== $argument) {
|
||||
$coreUtilities[$index] = $argument;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$needsArgumentResolver = true;
|
||||
}
|
||||
|
||||
if (!$needsArgumentResolver) {
|
||||
return $coreUtilities;
|
||||
}
|
||||
|
||||
if (null === $this->argumentResolver) {
|
||||
$this->argumentResolver = $this->command->getApplication()?->getArgumentResolver() ?? new ArgumentResolver(
|
||||
ArgumentResolver::getDefaultArgumentValueResolvers()
|
||||
);
|
||||
}
|
||||
|
||||
$closure = $function->getClosure();
|
||||
$resolvedArgs = $this->argumentResolver->getArguments($input, $closure, $function);
|
||||
|
||||
$parameters = [];
|
||||
$resolvedIndex = 0;
|
||||
|
||||
foreach ($function->getParameters() as $index => $param) {
|
||||
if (isset($coreUtilities[$index])) {
|
||||
$parameters[] = $coreUtilities[$index];
|
||||
} elseif ($param->isVariadic()) {
|
||||
// Variadic parameters consume all remaining resolved arguments
|
||||
$parameters = [...$parameters, ...\array_slice($resolvedArgs, $resolvedIndex)];
|
||||
break;
|
||||
} else {
|
||||
$parameters[] = $resolvedArgs[$resolvedIndex++] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
public function getSubscribedSignals(): array
|
||||
{
|
||||
return $this->signalableCommand?->getSubscribedSignals() ?? [];
|
||||
}
|
||||
|
||||
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
|
||||
{
|
||||
return $this->signalableCommand?->handleSignal($signal, $previousExitCode) ?? false;
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
if (null === $this->interactions) {
|
||||
$this->collectInteractions();
|
||||
}
|
||||
|
||||
return [] !== $this->interactions;
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
if (null === $this->interactions) {
|
||||
$this->collectInteractions();
|
||||
}
|
||||
|
||||
foreach ($this->interactions as $interaction) {
|
||||
$interaction->interact($input, $output, $this->getParameters(...));
|
||||
}
|
||||
}
|
||||
|
||||
private function collectInteractions(): void
|
||||
{
|
||||
$invokableThis = $this->invokable->getClosureThis();
|
||||
|
||||
$this->interactions = [];
|
||||
foreach ($this->invokable->getParameters() as $parameter) {
|
||||
if ($spec = Argument::tryFrom($parameter)) {
|
||||
if ($attribute = $spec->getInteractiveAttribute()) {
|
||||
$this->interactions[] = new Interaction($invokableThis, $attribute);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($spec = MapInput::tryFrom($parameter)) {
|
||||
$this->interactions = [...$this->interactions, ...$spec->getPropertyInteractions(), ...$spec->getMethodInteractions()];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$class = $this->invokable->getClosureCalledClass()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($class->getMethods() as $method) {
|
||||
if ($attribute = Interact::tryFrom($method)) {
|
||||
$this->interactions[] = new Interaction($invokableThis, $attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Helper\HelperInterface;
|
||||
use Symfony\Component\Console\Helper\HelperSet;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class LazyCommand extends Command
|
||||
{
|
||||
private \Closure|Command $command;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
array $aliases,
|
||||
string $description,
|
||||
bool $isHidden,
|
||||
\Closure $commandFactory,
|
||||
private ?bool $isEnabled = true,
|
||||
) {
|
||||
$this->setName($name)
|
||||
->setAliases($aliases)
|
||||
->setHidden($isHidden)
|
||||
->setDescription($description);
|
||||
|
||||
$this->command = $commandFactory;
|
||||
}
|
||||
|
||||
public function ignoreValidationErrors(): void
|
||||
{
|
||||
$this->getCommand()->ignoreValidationErrors();
|
||||
}
|
||||
|
||||
public function setApplication(?Application $application): void
|
||||
{
|
||||
if ($this->command instanceof parent) {
|
||||
$this->command->setApplication($application);
|
||||
}
|
||||
|
||||
parent::setApplication($application);
|
||||
}
|
||||
|
||||
public function setHelperSet(HelperSet $helperSet): void
|
||||
{
|
||||
if ($this->command instanceof parent) {
|
||||
$this->command->setHelperSet($helperSet);
|
||||
}
|
||||
|
||||
parent::setHelperSet($helperSet);
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->isEnabled ?? $this->getCommand()->isEnabled();
|
||||
}
|
||||
|
||||
public function run(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
return $this->getCommand()->run($input, $output);
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
$this->getCommand()->complete($input, $suggestions);
|
||||
}
|
||||
|
||||
public function setCode(callable $code): static
|
||||
{
|
||||
$this->getCommand()->setCode($code);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function mergeApplicationDefinition(bool $mergeArgs = true): void
|
||||
{
|
||||
$this->getCommand()->mergeApplicationDefinition($mergeArgs);
|
||||
}
|
||||
|
||||
public function setDefinition(array|InputDefinition $definition): static
|
||||
{
|
||||
$this->getCommand()->setDefinition($definition);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefinition(): InputDefinition
|
||||
{
|
||||
return $this->getCommand()->getDefinition();
|
||||
}
|
||||
|
||||
public function getNativeDefinition(): InputDefinition
|
||||
{
|
||||
return $this->getCommand()->getNativeDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
|
||||
*/
|
||||
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
|
||||
{
|
||||
$this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion
|
||||
*/
|
||||
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
|
||||
{
|
||||
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setProcessTitle(string $title): static
|
||||
{
|
||||
$this->getCommand()->setProcessTitle($title);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setHelp(string $help): static
|
||||
{
|
||||
$this->getCommand()->setHelp($help);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHelp(): string
|
||||
{
|
||||
return $this->getCommand()->getHelp();
|
||||
}
|
||||
|
||||
public function getProcessedHelp(): string
|
||||
{
|
||||
return $this->getCommand()->getProcessedHelp();
|
||||
}
|
||||
|
||||
public function getSynopsis(bool $short = false): string
|
||||
{
|
||||
return $this->getCommand()->getSynopsis($short);
|
||||
}
|
||||
|
||||
public function addUsage(string $usage): static
|
||||
{
|
||||
$this->getCommand()->addUsage($usage);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsages(): array
|
||||
{
|
||||
return $this->getCommand()->getUsages();
|
||||
}
|
||||
|
||||
public function getHelper(string $name): HelperInterface
|
||||
{
|
||||
return $this->getCommand()->getHelper($name);
|
||||
}
|
||||
|
||||
public function getCommand(): parent
|
||||
{
|
||||
if (!$this->command instanceof \Closure) {
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
$command = $this->command = ($this->command)();
|
||||
$command->setApplication($this->getApplication());
|
||||
|
||||
if (null !== $this->getHelperSet()) {
|
||||
$command->setHelperSet($this->getHelperSet());
|
||||
}
|
||||
|
||||
$command->setName($this->getName())
|
||||
->setAliases($this->getAliases())
|
||||
->setHidden($this->isHidden())
|
||||
->setDescription($this->getDescription());
|
||||
|
||||
// Will throw if the command is not correctly initialized.
|
||||
$command->getDefinition();
|
||||
|
||||
return $command;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Descriptor\ApplicationDescription;
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* ListCommand displays the list of all available commands for the application.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class ListCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('list')
|
||||
->setDefinition([
|
||||
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())),
|
||||
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
|
||||
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', static fn () => (new DescriptorHelper())->getFormats()),
|
||||
new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
|
||||
])
|
||||
->setDescription('List commands')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command lists all commands:
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
|
||||
You can also display the commands for a specific namespace:
|
||||
|
||||
<info>%command.full_name% test</info>
|
||||
|
||||
You can also output the information in other formats by using the <info>--format</info> option:
|
||||
|
||||
<info>%command.full_name% --format=xml</info>
|
||||
|
||||
It's also possible to get raw list of commands (useful for embedding command runner):
|
||||
|
||||
<info>%command.full_name% --raw</info>
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$helper = new DescriptorHelper();
|
||||
$helper->describe($output, $this->getApplication(), [
|
||||
'format' => $input->getOption('format'),
|
||||
'raw_text' => $input->getOption('raw'),
|
||||
'namespace' => $input->getArgument('namespace'),
|
||||
'short' => $input->getOption('short'),
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
use Symfony\Component\Lock\LockInterface;
|
||||
use Symfony\Component\Lock\Store\FlockStore;
|
||||
use Symfony\Component\Lock\Store\SemaphoreStore;
|
||||
|
||||
/**
|
||||
* Basic lock feature for commands.
|
||||
*
|
||||
* @author Geoffrey Brier <geoffrey.brier@gmail.com>
|
||||
*/
|
||||
trait LockableTrait
|
||||
{
|
||||
private ?LockInterface $lock = null;
|
||||
|
||||
private ?LockFactory $lockFactory = null;
|
||||
|
||||
/**
|
||||
* Locks a command.
|
||||
*/
|
||||
private function lock(?string $name = null, bool $blocking = false): bool
|
||||
{
|
||||
if (!class_exists(SemaphoreStore::class)) {
|
||||
throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".');
|
||||
}
|
||||
|
||||
if (null !== $this->lock) {
|
||||
throw new LogicException('A lock is already in place.');
|
||||
}
|
||||
|
||||
if (null === $this->lockFactory) {
|
||||
if (SemaphoreStore::isSupported()) {
|
||||
$store = new SemaphoreStore();
|
||||
} else {
|
||||
$store = new FlockStore();
|
||||
}
|
||||
|
||||
$this->lockFactory = new LockFactory($store);
|
||||
}
|
||||
|
||||
if (!$name) {
|
||||
if ($this instanceof Command) {
|
||||
$name = $this->getName();
|
||||
} elseif ($attribute = (new \ReflectionClass($this::class))->getAttributes(AsCommand::class)) {
|
||||
$name = $attribute[0]->newInstance()->name;
|
||||
} else {
|
||||
throw new LogicException(\sprintf('Lock name missing: provide it via "%s()", #[AsCommand] attribute, or by extending Command class.', __METHOD__));
|
||||
}
|
||||
}
|
||||
|
||||
$this->lock = $this->lockFactory->createLock($name);
|
||||
if (!$this->lock->acquire($blocking)) {
|
||||
$this->lock = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the command lock if there is one.
|
||||
*/
|
||||
private function release(): void
|
||||
{
|
||||
if ($this->lock) {
|
||||
$this->lock->release();
|
||||
$this->lock = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
/**
|
||||
* Interface for command reacting to signal.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrix.info>
|
||||
*/
|
||||
interface SignalableCommandInterface
|
||||
{
|
||||
/**
|
||||
* Returns the list of signals to subscribe.
|
||||
*
|
||||
* @return list<\SIG*>
|
||||
*
|
||||
* @see https://php.net/pcntl.constants for signals
|
||||
*/
|
||||
public function getSubscribedSignals(): array;
|
||||
|
||||
/**
|
||||
* The method will be called when the application is signaled.
|
||||
*
|
||||
* @return int|false The exit code to return or false to continue the normal execution
|
||||
*/
|
||||
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false;
|
||||
}
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Helper\HelperInterface;
|
||||
use Symfony\Component\Console\Helper\HelperSet;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Stopwatch\Stopwatch;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @author Jules Pietri <jules@heahprod.com>
|
||||
*/
|
||||
final class TraceableCommand extends Command
|
||||
{
|
||||
public readonly Command $command;
|
||||
public int $exitCode;
|
||||
public ?int $interruptedBySignal = null;
|
||||
public bool $ignoreValidation;
|
||||
public bool $isInteractive = false;
|
||||
public string $duration = 'n/a';
|
||||
public string $maxMemoryUsage = 'n/a';
|
||||
public InputInterface $input;
|
||||
public OutputInterface $output;
|
||||
/** @var array<string, mixed> */
|
||||
public array $arguments;
|
||||
/** @var array<string, mixed> */
|
||||
public array $options;
|
||||
/** @var array<string, mixed> */
|
||||
public array $interactiveInputs = [];
|
||||
public array $handledSignals = [];
|
||||
public ?array $invokableCommandInfo = null;
|
||||
|
||||
public function __construct(
|
||||
Command $command,
|
||||
private readonly Stopwatch $stopwatch,
|
||||
) {
|
||||
if ($command instanceof LazyCommand) {
|
||||
$command = $command->getCommand();
|
||||
}
|
||||
|
||||
$this->command = $command;
|
||||
|
||||
// prevent call to self::getDefaultDescription()
|
||||
$this->setDescription($command->getDescription());
|
||||
|
||||
parent::__construct($command->getName());
|
||||
|
||||
// init below enables calling {@see parent::run()}
|
||||
[$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(fn () => [$this->code, $this->processTitle, $this->ignoreValidationErrors], $command, Command::class)();
|
||||
|
||||
if (\is_callable($code)) {
|
||||
$this->setCode($code);
|
||||
}
|
||||
|
||||
if ($processTitle) {
|
||||
parent::setProcessTitle($processTitle);
|
||||
}
|
||||
|
||||
if ($ignoreValidationErrors) {
|
||||
parent::ignoreValidationErrors();
|
||||
}
|
||||
|
||||
$this->ignoreValidation = $ignoreValidationErrors;
|
||||
}
|
||||
|
||||
public function __call(string $name, array $arguments): mixed
|
||||
{
|
||||
return $this->command->{$name}(...$arguments);
|
||||
}
|
||||
|
||||
public function getSubscribedSignals(): array
|
||||
{
|
||||
return $this->command->getSubscribedSignals();
|
||||
}
|
||||
|
||||
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
|
||||
{
|
||||
$event = $this->stopwatch->start($this->getName().'.handle_signal');
|
||||
|
||||
$exit = $this->command->handleSignal($signal, $previousExitCode);
|
||||
|
||||
$event->stop();
|
||||
|
||||
if (!isset($this->handledSignals[$signal])) {
|
||||
$this->handledSignals[$signal] = [
|
||||
'handled' => 0,
|
||||
'duration' => 0,
|
||||
'memory' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
++$this->handledSignals[$signal]['handled'];
|
||||
$this->handledSignals[$signal]['duration'] += $event->getDuration();
|
||||
$this->handledSignals[$signal]['memory'] = max(
|
||||
$this->handledSignals[$signal]['memory'],
|
||||
$event->getMemory() >> 20
|
||||
);
|
||||
|
||||
return $exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Calling parent method is required to be used in {@see parent::run()}.
|
||||
*/
|
||||
public function ignoreValidationErrors(): void
|
||||
{
|
||||
$this->ignoreValidation = true;
|
||||
$this->command->ignoreValidationErrors();
|
||||
|
||||
parent::ignoreValidationErrors();
|
||||
}
|
||||
|
||||
public function setApplication(?Application $application = null): void
|
||||
{
|
||||
$this->command->setApplication($application);
|
||||
}
|
||||
|
||||
public function getApplication(): ?Application
|
||||
{
|
||||
return $this->command->getApplication();
|
||||
}
|
||||
|
||||
public function setHelperSet(HelperSet $helperSet): void
|
||||
{
|
||||
$this->command->setHelperSet($helperSet);
|
||||
}
|
||||
|
||||
public function getHelperSet(): ?HelperSet
|
||||
{
|
||||
return $this->command->getHelperSet();
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->command->isEnabled();
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
$this->command->complete($input, $suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Calling parent method is required to be used in {@see parent::run()}.
|
||||
*/
|
||||
public function setCode(callable $code): static
|
||||
{
|
||||
if ($code instanceof InvokableCommand) {
|
||||
$r = \Closure::bind(fn () => $this->invokable, $code, InvokableCommand::class)();
|
||||
|
||||
$this->invokableCommandInfo = [
|
||||
'class' => $r->getClosureScopeClass()->name,
|
||||
'file' => $r->getFileName(),
|
||||
'line' => $r->getStartLine(),
|
||||
];
|
||||
|
||||
// Pass the original callable to avoid double-wrapping in Command::setCode()
|
||||
$this->command->setCode($code->getCode());
|
||||
} else {
|
||||
$this->command->setCode($code);
|
||||
}
|
||||
|
||||
return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int {
|
||||
$event = $this->stopwatch->start($this->getName().'.code');
|
||||
|
||||
$this->exitCode = $code($input, $output);
|
||||
|
||||
$event->stop();
|
||||
|
||||
return $this->exitCode;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function mergeApplicationDefinition(bool $mergeArgs = true): void
|
||||
{
|
||||
$this->command->mergeApplicationDefinition($mergeArgs);
|
||||
}
|
||||
|
||||
public function setDefinition(array|InputDefinition $definition): static
|
||||
{
|
||||
$this->command->setDefinition($definition);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefinition(): InputDefinition
|
||||
{
|
||||
return $this->command->getDefinition();
|
||||
}
|
||||
|
||||
public function getNativeDefinition(): InputDefinition
|
||||
{
|
||||
return $this->command->getNativeDefinition();
|
||||
}
|
||||
|
||||
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
|
||||
{
|
||||
$this->command->addArgument($name, $mode, $description, $default, $suggestedValues);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
|
||||
{
|
||||
$this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Calling parent method is required to be used in {@see parent::run()}.
|
||||
*/
|
||||
public function setProcessTitle(string $title): static
|
||||
{
|
||||
$this->command->setProcessTitle($title);
|
||||
|
||||
return parent::setProcessTitle($title);
|
||||
}
|
||||
|
||||
public function setHelp(string $help): static
|
||||
{
|
||||
$this->command->setHelp($help);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHelp(): string
|
||||
{
|
||||
return $this->command->getHelp();
|
||||
}
|
||||
|
||||
public function getProcessedHelp(): string
|
||||
{
|
||||
return $this->command->getProcessedHelp();
|
||||
}
|
||||
|
||||
public function getSynopsis(bool $short = false): string
|
||||
{
|
||||
return $this->command->getSynopsis($short);
|
||||
}
|
||||
|
||||
public function addUsage(string $usage): static
|
||||
{
|
||||
$this->command->addUsage($usage);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsages(): array
|
||||
{
|
||||
return $this->command->getUsages();
|
||||
}
|
||||
|
||||
public function getHelper(string $name): HelperInterface
|
||||
{
|
||||
return $this->command->getHelper($name);
|
||||
}
|
||||
|
||||
public function run(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$initialArguments = $input->getArguments();
|
||||
$initialOptions = $input->getOptions();
|
||||
$event = $this->stopwatch->start($this->getName(), 'command');
|
||||
|
||||
try {
|
||||
$this->exitCode = $this->command->run($input, $output);
|
||||
} finally {
|
||||
$event->stop();
|
||||
|
||||
if ($output instanceof ConsoleOutputInterface && $output->isDebug()) {
|
||||
$output->getErrorOutput()->writeln((string) $event);
|
||||
}
|
||||
|
||||
$this->duration = $event->getDuration().' ms';
|
||||
$this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB';
|
||||
|
||||
$this->arguments = $input->getArguments();
|
||||
$this->options = $input->getOptions();
|
||||
|
||||
$this->extractInteractiveInputs($initialArguments, $initialOptions);
|
||||
$this->isInteractive = $this->isInteractive || $this->interactiveInputs;
|
||||
}
|
||||
|
||||
return $this->exitCode;
|
||||
}
|
||||
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$event = $this->stopwatch->start($this->getName().'.init', 'command');
|
||||
|
||||
$this->command->initialize($input, $output);
|
||||
|
||||
$event->stop();
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->class) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = $this->stopwatch->start($this->getName().'.interact', 'command');
|
||||
|
||||
$this->command->interact($input, $output);
|
||||
|
||||
$event->stop();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$event = $this->stopwatch->start($this->getName().'.execute', 'command');
|
||||
|
||||
$exitCode = $this->command->execute($input, $output);
|
||||
|
||||
$event->stop();
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
private function extractInteractiveInputs(array $initialArguments, array $initialOptions): void
|
||||
{
|
||||
$nativeDefinition = $this->command->getNativeDefinition();
|
||||
|
||||
foreach ($nativeDefinition->getArguments() as $argName => $argument) {
|
||||
if (\array_key_exists($argName, $initialArguments) && $initialArguments[$argName] === $this->arguments[$argName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->interactiveInputs[$argName] = $this->arguments[$argName];
|
||||
}
|
||||
|
||||
foreach ($nativeDefinition->getOptions() as $optName => $option) {
|
||||
if (\array_key_exists($optName, $initialOptions) && $initialOptions[$optName] === $this->options[$optName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->interactiveInputs['--'.$optName] = $this->options[$optName];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\CommandLoader;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
|
||||
/**
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
interface CommandLoaderInterface
|
||||
{
|
||||
/**
|
||||
* Loads a command.
|
||||
*
|
||||
* @throws CommandNotFoundException
|
||||
*/
|
||||
public function get(string $name): Command;
|
||||
|
||||
/**
|
||||
* Checks if a command exists.
|
||||
*/
|
||||
public function has(string $name): bool;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNames(): array;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\CommandLoader;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
|
||||
/**
|
||||
* Loads commands from a PSR-11 container.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
class ContainerCommandLoader implements CommandLoaderInterface
|
||||
{
|
||||
/**
|
||||
* @param array $commandMap An array with command names as keys and service ids as values
|
||||
*/
|
||||
public function __construct(
|
||||
private ContainerInterface $container,
|
||||
private array $commandMap,
|
||||
) {
|
||||
}
|
||||
|
||||
public function get(string $name): Command
|
||||
{
|
||||
if (!$this->has($name)) {
|
||||
throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
return $this->container->get($this->commandMap[$name]);
|
||||
}
|
||||
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]);
|
||||
}
|
||||
|
||||
public function getNames(): array
|
||||
{
|
||||
return array_keys($this->commandMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\CommandLoader;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
|
||||
/**
|
||||
* A simple command loader using factories to instantiate commands lazily.
|
||||
*
|
||||
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
|
||||
*/
|
||||
class FactoryCommandLoader implements CommandLoaderInterface
|
||||
{
|
||||
/**
|
||||
* @param callable[] $factories Indexed by command names
|
||||
*/
|
||||
public function __construct(
|
||||
private array $factories,
|
||||
) {
|
||||
}
|
||||
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return isset($this->factories[$name]);
|
||||
}
|
||||
|
||||
public function get(string $name): Command
|
||||
{
|
||||
if (!isset($this->factories[$name])) {
|
||||
throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
$factory = $this->factories[$name];
|
||||
|
||||
return $factory();
|
||||
}
|
||||
|
||||
public function getNames(): array
|
||||
{
|
||||
return array_keys($this->factories);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Completion;
|
||||
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* An input specialized for shell completion.
|
||||
*
|
||||
* This input allows unfinished option names or values and exposes what kind of
|
||||
* completion is expected.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
final class CompletionInput extends ArgvInput
|
||||
{
|
||||
public const TYPE_ARGUMENT_VALUE = 'argument_value';
|
||||
public const TYPE_OPTION_VALUE = 'option_value';
|
||||
public const TYPE_OPTION_NAME = 'option_name';
|
||||
public const TYPE_NONE = 'none';
|
||||
|
||||
private array $tokens;
|
||||
private int $currentIndex;
|
||||
private string $completionType;
|
||||
private ?string $completionName = null;
|
||||
private string $completionValue = '';
|
||||
|
||||
/**
|
||||
* Converts a terminal string into tokens.
|
||||
*
|
||||
* This is required for shell completions without COMP_WORDS support.
|
||||
*/
|
||||
public static function fromString(string $inputStr, int $currentIndex): self
|
||||
{
|
||||
preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?<!\\\\)\1(?=$|\s)/', $inputStr, $tokens);
|
||||
|
||||
return self::fromTokens($tokens[0], $currentIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an input based on an COMP_WORDS token list.
|
||||
*
|
||||
* @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv)
|
||||
* @param int $currentIndex the index of the cursor (e.g. COMP_CWORD)
|
||||
*/
|
||||
public static function fromTokens(array $tokens, int $currentIndex): self
|
||||
{
|
||||
$input = new self($tokens);
|
||||
$input->tokens = $tokens;
|
||||
$input->currentIndex = $currentIndex;
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
public function bind(InputDefinition $definition): void
|
||||
{
|
||||
parent::bind($definition);
|
||||
|
||||
$relevantToken = $this->getRelevantToken();
|
||||
if ('-' === $relevantToken[0]) {
|
||||
// the current token is an input option: complete either option name or option value
|
||||
[$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', ''];
|
||||
|
||||
$option = $this->getOptionFromToken($optionToken);
|
||||
if (null === $option && !$this->isCursorFree()) {
|
||||
$this->completionType = self::TYPE_OPTION_NAME;
|
||||
$this->completionValue = $relevantToken;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($option?->acceptValue()) {
|
||||
$this->completionType = self::TYPE_OPTION_VALUE;
|
||||
$this->completionName = $option->getName();
|
||||
$this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : '');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$previousToken = $this->tokens[$this->currentIndex - 1];
|
||||
if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
|
||||
// check if previous option accepted a value
|
||||
$previousOption = $this->getOptionFromToken($previousToken);
|
||||
if ($previousOption?->acceptValue()) {
|
||||
$this->completionType = self::TYPE_OPTION_VALUE;
|
||||
$this->completionName = $previousOption->getName();
|
||||
$this->completionValue = $relevantToken;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// complete argument value
|
||||
$this->completionType = self::TYPE_ARGUMENT_VALUE;
|
||||
|
||||
foreach ($this->definition->getArguments() as $argumentName => $argument) {
|
||||
if (!isset($this->arguments[$argumentName])) {
|
||||
break;
|
||||
}
|
||||
|
||||
$argumentValue = $this->arguments[$argumentName];
|
||||
$this->completionName = $argumentName;
|
||||
if (\is_array($argumentValue)) {
|
||||
$this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null;
|
||||
} else {
|
||||
$this->completionValue = $argumentValue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->currentIndex >= \count($this->tokens)) {
|
||||
if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
|
||||
$this->completionName = $argumentName;
|
||||
} else {
|
||||
// we've reached the end
|
||||
$this->completionType = self::TYPE_NONE;
|
||||
$this->completionName = null;
|
||||
}
|
||||
|
||||
$this->completionValue = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of completion required.
|
||||
*
|
||||
* TYPE_ARGUMENT_VALUE when completing the value of an input argument
|
||||
* TYPE_OPTION_VALUE when completing the value of an input option
|
||||
* TYPE_OPTION_NAME when completing the name of an input option
|
||||
* TYPE_NONE when nothing should be completed
|
||||
*
|
||||
* TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component.
|
||||
*
|
||||
* @return self::TYPE_*
|
||||
*/
|
||||
public function getCompletionType(): string
|
||||
{
|
||||
return $this->completionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the input option or argument when completing a value.
|
||||
*
|
||||
* @return string|null returns null when completing an option name
|
||||
*/
|
||||
public function getCompletionName(): ?string
|
||||
{
|
||||
return $this->completionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The value already typed by the user (or empty string).
|
||||
*/
|
||||
public function getCompletionValue(): string
|
||||
{
|
||||
return $this->completionValue;
|
||||
}
|
||||
|
||||
public function mustSuggestOptionValuesFor(string $optionName): bool
|
||||
{
|
||||
return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName();
|
||||
}
|
||||
|
||||
public function mustSuggestArgumentValuesFor(string $argumentName): bool
|
||||
{
|
||||
return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName();
|
||||
}
|
||||
|
||||
protected function parseToken(string $token, bool $parseOptions): bool
|
||||
{
|
||||
try {
|
||||
return parent::parseToken($token, $parseOptions);
|
||||
} catch (RuntimeException) {
|
||||
// suppress errors, completed input is almost never valid
|
||||
}
|
||||
|
||||
return $parseOptions;
|
||||
}
|
||||
|
||||
private function getOptionFromToken(string $optionToken): ?InputOption
|
||||
{
|
||||
$optionName = ltrim($optionToken, '-');
|
||||
if (!$optionName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('-' === ($optionToken[1] ?? ' ')) {
|
||||
// long option name
|
||||
return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null;
|
||||
}
|
||||
|
||||
// short option name
|
||||
return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The token of the cursor, or the last token if the cursor is at the end of the input.
|
||||
*/
|
||||
private function getRelevantToken(): string
|
||||
{
|
||||
return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the cursor is "free" (i.e. at the end of the input preceded by a space).
|
||||
*/
|
||||
private function isCursorFree(): bool
|
||||
{
|
||||
$nrOfTokens = \count($this->tokens);
|
||||
if ($this->currentIndex > $nrOfTokens) {
|
||||
throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.');
|
||||
}
|
||||
|
||||
return $this->currentIndex >= $nrOfTokens;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$str = '';
|
||||
foreach ($this->tokens as $i => $token) {
|
||||
$str .= $token;
|
||||
|
||||
if ($this->currentIndex === $i) {
|
||||
$str .= '|';
|
||||
}
|
||||
|
||||
$str .= ' ';
|
||||
}
|
||||
|
||||
if ($this->currentIndex > $i) {
|
||||
$str .= '|';
|
||||
}
|
||||
|
||||
return rtrim($str);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Completion;
|
||||
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Stores all completion suggestions for the current input.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
final class CompletionSuggestions
|
||||
{
|
||||
private array $valueSuggestions = [];
|
||||
private array $optionSuggestions = [];
|
||||
|
||||
/**
|
||||
* Add a suggested value for an input option or argument.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function suggestValue(string|Suggestion $value): static
|
||||
{
|
||||
$this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple suggested values at once for an input option or argument.
|
||||
*
|
||||
* @param list<string|Suggestion> $values
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function suggestValues(array $values): static
|
||||
{
|
||||
foreach ($values as $value) {
|
||||
$this->suggestValue($value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a suggestion for an input option name.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function suggestOption(InputOption $option): static
|
||||
{
|
||||
$this->optionSuggestions[] = $option;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple suggestions for input option names at once.
|
||||
*
|
||||
* @param InputOption[] $options
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function suggestOptions(array $options): static
|
||||
{
|
||||
foreach ($options as $option) {
|
||||
$this->suggestOption($option);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputOption[]
|
||||
*/
|
||||
public function getOptionSuggestions(): array
|
||||
{
|
||||
return $this->optionSuggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Suggestion[]
|
||||
*/
|
||||
public function getValueSuggestions(): array
|
||||
{
|
||||
return $this->valueSuggestions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Completion\Output;
|
||||
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class BashCompletionOutput implements CompletionOutputInterface
|
||||
{
|
||||
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
|
||||
{
|
||||
$values = $suggestions->getValueSuggestions();
|
||||
foreach ($suggestions->getOptionSuggestions() as $option) {
|
||||
$values[] = '--'.$option->getName();
|
||||
if ($option->isNegatable()) {
|
||||
$values[] = '--no-'.$option->getName();
|
||||
}
|
||||
}
|
||||
$output->writeln(implode("\n", $values));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Completion\Output;
|
||||
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Transforms the {@see CompletionSuggestions} object into output readable by the shell completion.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
interface CompletionOutputInterface
|
||||
{
|
||||
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Completion\Output;
|
||||
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Guillaume Aveline <guillaume.aveline@pm.me>
|
||||
*/
|
||||
class FishCompletionOutput implements CompletionOutputInterface
|
||||
{
|
||||
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
|
||||
{
|
||||
$values = [];
|
||||
foreach ($suggestions->getValueSuggestions() as $value) {
|
||||
$values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
|
||||
}
|
||||
foreach ($suggestions->getOptionSuggestions() as $option) {
|
||||
$values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
|
||||
if ($option->isNegatable()) {
|
||||
$values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
|
||||
}
|
||||
}
|
||||
$output->write(implode("\n", $values));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Completion\Output;
|
||||
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Jitendra A <adhocore@gmail.com>
|
||||
*/
|
||||
class ZshCompletionOutput implements CompletionOutputInterface
|
||||
{
|
||||
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
|
||||
{
|
||||
$values = [];
|
||||
foreach ($suggestions->getValueSuggestions() as $value) {
|
||||
$values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
|
||||
}
|
||||
foreach ($suggestions->getOptionSuggestions() as $option) {
|
||||
$values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
|
||||
if ($option->isNegatable()) {
|
||||
$values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
|
||||
}
|
||||
}
|
||||
$output->write(implode("\n", $values)."\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Completion;
|
||||
|
||||
/**
|
||||
* Represents a single suggested value.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class Suggestion implements \Stringable
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $value,
|
||||
private readonly string $description = '',
|
||||
) {
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getValue();
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console;
|
||||
|
||||
use Symfony\Component\Config\Resource\ClassExistenceResource;
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\ValueResolverInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Attribute\AsTargetedValueResolver;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
|
||||
use Symfony\Component\Console\DependencyInjection\ConsoleArgumentValueResolverPass;
|
||||
use Symfony\Component\Console\DependencyInjection\RegisterCommandArgumentLocatorsPass;
|
||||
use Symfony\Component\Console\DependencyInjection\RemoveEmptyCommandArgumentLocatorsPass;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Kernel\AbstractBundle;
|
||||
use Symfony\Component\DependencyInjection\Kernel\RequiredBundle;
|
||||
use Symfony\Component\DependencyInjection\Kernel\ServicesBundle;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand;
|
||||
use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
#[RequiredBundle(ServicesBundle::class)]
|
||||
class ConsoleBundle extends AbstractBundle
|
||||
{
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path ??= __DIR__;
|
||||
}
|
||||
|
||||
public function build(ContainerBuilder $container): void
|
||||
{
|
||||
$this->addCompilerPassIfExists($container, AddEventAliasesPass::class, [ConsoleEvents::ALIASES, [], [ConsoleEvents::COMMAND, ConsoleEvents::TERMINATE, ConsoleEvents::ERROR]]);
|
||||
$container->addCompilerPass(new RegisterCommandArgumentLocatorsPass());
|
||||
$container->addCompilerPass(new RemoveEmptyCommandArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);
|
||||
$container->addCompilerPass(new ConsoleArgumentValueResolverPass());
|
||||
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
|
||||
}
|
||||
|
||||
public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void
|
||||
{
|
||||
$configurator->import('Resources/config/console.php');
|
||||
|
||||
$container->registerForAutoconfiguration(Command::class)
|
||||
->addTag('console.command')
|
||||
->addTag('console.command.service_arguments');
|
||||
$container->registerForAutoconfiguration(ValueResolverInterface::class)
|
||||
->addTag('console.argument_value_resolver');
|
||||
$container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute, \ReflectionClass|\ReflectionMethod $reflector) {
|
||||
$tagAttributes = [
|
||||
'command' => $attribute->name,
|
||||
'description' => $attribute->description,
|
||||
'help' => $attribute->help ?? null,
|
||||
];
|
||||
|
||||
if ($reflector instanceof \ReflectionMethod) {
|
||||
$tagAttributes['method'] = $reflector->getName();
|
||||
}
|
||||
|
||||
$definition->addTag('console.command', $tagAttributes);
|
||||
$definition->addTag('console.command.service_arguments');
|
||||
});
|
||||
$container->registerAttributeForAutoconfiguration(AsTargetedValueResolver::class, static function (ChildDefinition $definition, AsTargetedValueResolver $attribute): void {
|
||||
$definition->addTag('console.targeted_value_resolver', $attribute->name ? ['name' => $attribute->name] : []);
|
||||
});
|
||||
|
||||
if (!class_exists(DotenvDebugCommand::class)) {
|
||||
$container->removeDefinition('console.command.dotenv_debug');
|
||||
}
|
||||
|
||||
if (!interface_exists(EventDispatcherInterface::class)) {
|
||||
$container->removeDefinition('console.error_listener');
|
||||
}
|
||||
}
|
||||
|
||||
private function addCompilerPassIfExists(ContainerBuilder $container, string $class, array $arguments = []): void
|
||||
{
|
||||
$container->addResource(new ClassExistenceResource($class));
|
||||
|
||||
if (class_exists($class)) {
|
||||
$container->addCompilerPass(new $class(...$arguments));
|
||||
}
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console;
|
||||
|
||||
use Symfony\Component\Console\Event\ConsoleCommandEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleErrorEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleSignalEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
use Symfony\Component\Console\Event\QuestionAnsweredEvent;
|
||||
|
||||
/**
|
||||
* Contains all events dispatched by an Application.
|
||||
*
|
||||
* @author Francesco Levorato <git@flevour.net>
|
||||
*/
|
||||
final class ConsoleEvents
|
||||
{
|
||||
/**
|
||||
* The COMMAND event allows you to attach listeners before any command is
|
||||
* executed by the console. It also allows you to modify the command, input and output
|
||||
* before they are handed to the command.
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleCommandEvent")
|
||||
*/
|
||||
public const COMMAND = 'console.command';
|
||||
|
||||
/**
|
||||
* The SIGNAL event allows you to perform some actions
|
||||
* after the command execution was interrupted.
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleSignalEvent")
|
||||
*/
|
||||
public const SIGNAL = 'console.signal';
|
||||
|
||||
/**
|
||||
* The TERMINATE event allows you to attach listeners after a command is
|
||||
* executed by the console.
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent")
|
||||
*/
|
||||
public const TERMINATE = 'console.terminate';
|
||||
|
||||
/**
|
||||
* The ERROR event occurs when an uncaught exception or error appears.
|
||||
*
|
||||
* This event allows you to deal with the exception/error or
|
||||
* to modify the thrown exception.
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleErrorEvent")
|
||||
*/
|
||||
public const ERROR = 'console.error';
|
||||
|
||||
/**
|
||||
* The QUESTION_ANSWERED event allows you to validate user input
|
||||
* using Symfony Validator constraints.
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\QuestionAnsweredEvent")
|
||||
*/
|
||||
public const QUESTION_ANSWERED = 'console.question_answered';
|
||||
|
||||
/**
|
||||
* Event aliases.
|
||||
*
|
||||
* These aliases can be consumed by RegisterListenersPass.
|
||||
*/
|
||||
public const ALIASES = [
|
||||
ConsoleCommandEvent::class => self::COMMAND,
|
||||
ConsoleErrorEvent::class => self::ERROR,
|
||||
ConsoleSignalEvent::class => self::SIGNAL,
|
||||
ConsoleTerminateEvent::class => self::TERMINATE,
|
||||
QuestionAnsweredEvent::class => self::QUESTION_ANSWERED,
|
||||
];
|
||||
}
|
||||
Vendored
+204
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Pierre du Plessis <pdples@gmail.com>
|
||||
*/
|
||||
final class Cursor
|
||||
{
|
||||
/** @var resource */
|
||||
private $input;
|
||||
|
||||
/**
|
||||
* @param resource|null $input
|
||||
*/
|
||||
public function __construct(
|
||||
private OutputInterface $output,
|
||||
$input = null,
|
||||
) {
|
||||
$this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function moveUp(int $lines = 1): static
|
||||
{
|
||||
$this->output->write(\sprintf("\x1b[%dA", $lines));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function moveDown(int $lines = 1): static
|
||||
{
|
||||
$this->output->write(\sprintf("\x1b[%dB", $lines));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function moveRight(int $columns = 1): static
|
||||
{
|
||||
$this->output->write(\sprintf("\x1b[%dC", $columns));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function moveLeft(int $columns = 1): static
|
||||
{
|
||||
$this->output->write(\sprintf("\x1b[%dD", $columns));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function moveToColumn(int $column): static
|
||||
{
|
||||
$this->output->write(\sprintf("\x1b[%dG", $column));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function moveToPosition(int $column, int $row): static
|
||||
{
|
||||
$this->output->write(\sprintf("\x1b[%d;%dH", $row + 1, $column));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function savePosition(): static
|
||||
{
|
||||
$this->output->write("\x1b7");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function restorePosition(): static
|
||||
{
|
||||
$this->output->write("\x1b8");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function hide(): static
|
||||
{
|
||||
$this->output->write("\x1b[?25l");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function show(): static
|
||||
{
|
||||
$this->output->write("\x1b[?25h\x1b[?0c");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the output from the current line.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function clearLine(): static
|
||||
{
|
||||
$this->output->write("\x1b[2K");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the output from the current line after the current position.
|
||||
*/
|
||||
public function clearLineAfter(): self
|
||||
{
|
||||
$this->output->write("\x1b[K");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the output from the cursors' current position to the end of the screen.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function clearOutput(): static
|
||||
{
|
||||
$this->output->write("\x1b[0J");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire screen.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function clearScreen(): static
|
||||
{
|
||||
$this->output->write("\x1b[2J");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current cursor position as x,y coordinates.
|
||||
*/
|
||||
public function getCurrentPosition(): array
|
||||
{
|
||||
static $isTtySupported;
|
||||
|
||||
if (!$isTtySupported ??= '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)) {
|
||||
return [1, 1];
|
||||
}
|
||||
|
||||
$sttyMode = shell_exec('stty -g');
|
||||
shell_exec('stty -icanon -echo');
|
||||
|
||||
@fwrite($this->input, "\033[6n");
|
||||
|
||||
$code = trim(fread($this->input, 1024));
|
||||
|
||||
shell_exec(\sprintf('stty %s', $sttyMode));
|
||||
|
||||
sscanf($code, "\033[%d;%dR", $row, $col);
|
||||
|
||||
return [$col, $row];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\DataCollector;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Debug\CliRequest;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\SignalRegistry\SignalMap;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\VarDumper\Cloner\Data;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @author Jules Pietri <jules@heahprod.com>
|
||||
*/
|
||||
final class CommandDataCollector extends DataCollector
|
||||
{
|
||||
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
|
||||
{
|
||||
if (!$request instanceof CliRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
$command = $request->command;
|
||||
$application = $command->getApplication();
|
||||
|
||||
$this->data = [
|
||||
'command' => $command->invokableCommandInfo ?? $this->cloneVar($command->command),
|
||||
'exit_code' => $command->exitCode,
|
||||
'interrupted_by_signal' => $command->interruptedBySignal,
|
||||
'duration' => $command->duration,
|
||||
'max_memory_usage' => $command->maxMemoryUsage,
|
||||
'verbosity_level' => match ($command->output->getVerbosity()) {
|
||||
OutputInterface::VERBOSITY_SILENT => 'silent',
|
||||
OutputInterface::VERBOSITY_QUIET => 'quiet',
|
||||
OutputInterface::VERBOSITY_NORMAL => 'normal',
|
||||
OutputInterface::VERBOSITY_VERBOSE => 'verbose',
|
||||
OutputInterface::VERBOSITY_VERY_VERBOSE => 'very verbose',
|
||||
OutputInterface::VERBOSITY_DEBUG => 'debug',
|
||||
},
|
||||
'interactive' => $command->isInteractive,
|
||||
'validate_input' => !$command->ignoreValidation,
|
||||
'enabled' => $command->isEnabled(),
|
||||
'visible' => !$command->isHidden(),
|
||||
'input' => $this->cloneVar($command->input),
|
||||
'output' => $this->cloneVar($command->output),
|
||||
'interactive_inputs' => array_map($this->cloneVar(...), $command->interactiveInputs),
|
||||
'signalable' => $command->getSubscribedSignals(),
|
||||
'handled_signals' => $command->handledSignals,
|
||||
'helper_set' => array_map($this->cloneVar(...), iterator_to_array($command->getHelperSet())),
|
||||
];
|
||||
|
||||
$baseDefinition = $application->getDefinition();
|
||||
|
||||
foreach ($command->arguments as $argName => $argValue) {
|
||||
if ($baseDefinition->hasArgument($argName)) {
|
||||
$this->data['application_inputs'][$argName] = $this->cloneVar($argValue);
|
||||
} else {
|
||||
$this->data['arguments'][$argName] = $this->cloneVar($argValue);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($command->options as $optName => $optValue) {
|
||||
if ($baseDefinition->hasOption($optName)) {
|
||||
$this->data['application_inputs']['--'.$optName] = $this->cloneVar($optValue);
|
||||
} else {
|
||||
$this->data['options'][$optName] = $this->cloneVar($optValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'command';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* class?: class-string,
|
||||
* executor?: string,
|
||||
* file: string,
|
||||
* line: int,
|
||||
* }
|
||||
*/
|
||||
public function getCommand(): array
|
||||
{
|
||||
if (\is_array($this->data['command'])) {
|
||||
return $this->data['command'];
|
||||
}
|
||||
|
||||
$class = $this->data['command']->getType();
|
||||
$r = new \ReflectionMethod($class, 'execute');
|
||||
|
||||
if (Command::class !== $r->getDeclaringClass()) {
|
||||
return [
|
||||
'executor' => $class.'::'.$r->name,
|
||||
'file' => $r->getFileName(),
|
||||
'line' => $r->getStartLine(),
|
||||
];
|
||||
}
|
||||
|
||||
$r = new \ReflectionClass($class);
|
||||
|
||||
return [
|
||||
'class' => $class,
|
||||
'file' => $r->getFileName(),
|
||||
'line' => $r->getStartLine(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getInterruptedBySignal(): ?string
|
||||
{
|
||||
if (isset($this->data['interrupted_by_signal'])) {
|
||||
return \sprintf('%s (%d)', SignalMap::getSignalName($this->data['interrupted_by_signal']), $this->data['interrupted_by_signal']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getDuration(): string
|
||||
{
|
||||
return $this->data['duration'];
|
||||
}
|
||||
|
||||
public function getMaxMemoryUsage(): string
|
||||
{
|
||||
return $this->data['max_memory_usage'];
|
||||
}
|
||||
|
||||
public function getVerbosityLevel(): string
|
||||
{
|
||||
return $this->data['verbosity_level'];
|
||||
}
|
||||
|
||||
public function getInteractive(): bool
|
||||
{
|
||||
return $this->data['interactive'];
|
||||
}
|
||||
|
||||
public function getValidateInput(): bool
|
||||
{
|
||||
return $this->data['validate_input'];
|
||||
}
|
||||
|
||||
public function getEnabled(): bool
|
||||
{
|
||||
return $this->data['enabled'];
|
||||
}
|
||||
|
||||
public function getVisible(): bool
|
||||
{
|
||||
return $this->data['visible'];
|
||||
}
|
||||
|
||||
public function getInput(): Data
|
||||
{
|
||||
return $this->data['input'];
|
||||
}
|
||||
|
||||
public function getOutput(): Data
|
||||
{
|
||||
return $this->data['output'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data[]
|
||||
*/
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->data['arguments'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data[]
|
||||
*/
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->data['options'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data[]
|
||||
*/
|
||||
public function getApplicationInputs(): array
|
||||
{
|
||||
return $this->data['application_inputs'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data[]
|
||||
*/
|
||||
public function getInteractiveInputs(): array
|
||||
{
|
||||
return $this->data['interactive_inputs'] ?? [];
|
||||
}
|
||||
|
||||
public function getSignalable(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
|
||||
$this->data['signalable']
|
||||
);
|
||||
}
|
||||
|
||||
public function getHandledSignals(): array
|
||||
{
|
||||
$keys = array_map(
|
||||
static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
|
||||
array_keys($this->data['handled_signals'])
|
||||
);
|
||||
|
||||
return array_combine($keys, array_values($this->data['handled_signals']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Data[]
|
||||
*/
|
||||
public function getHelperSet(): array
|
||||
{
|
||||
return $this->data['helper_set'] ?? [];
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->data = [];
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Debug;
|
||||
|
||||
use Symfony\Component\Console\Command\TraceableCommand;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class CliRequest extends Request
|
||||
{
|
||||
public function __construct(
|
||||
public readonly TraceableCommand $command,
|
||||
) {
|
||||
parent::__construct(
|
||||
attributes: ['_controller' => $command->command::class, '_virtual_type' => 'command'],
|
||||
server: $_SERVER,
|
||||
);
|
||||
}
|
||||
|
||||
// Methods below allow to populate a profile, thus enable search and filtering
|
||||
public function getUri(): string
|
||||
{
|
||||
if ($this->server->has('SYMFONY_CLI_BINARY_NAME')) {
|
||||
$binary = $this->server->get('SYMFONY_CLI_BINARY_NAME').' console';
|
||||
} else {
|
||||
$binary = $this->server->get('argv')[0];
|
||||
}
|
||||
|
||||
return $binary.' '.$this->command->input;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->command->isInteractive ? 'INTERACTIVE' : 'BATCH';
|
||||
}
|
||||
|
||||
public function getResponse(): Response
|
||||
{
|
||||
return new class($this->command->exitCode) extends Response {
|
||||
public function __construct(private readonly int $exitCode)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public function getClientIp(): string
|
||||
{
|
||||
$application = $this->command->getApplication();
|
||||
|
||||
return $application->getName().' '.$application->getVersion();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Command\LazyCommand;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\TypedReference;
|
||||
|
||||
/**
|
||||
* Registers console commands.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class AddConsoleCommandPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$commandServices = [];
|
||||
$lazyCommandMap = [];
|
||||
$lazyCommandRefs = [];
|
||||
$serviceIds = [];
|
||||
|
||||
foreach ($container->findTaggedServiceIds('console.command', true) as $id => $tags) {
|
||||
foreach ($tags as $tag) {
|
||||
$commandServices[$id][$tag['method'] ?? '__invoke'][] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($commandServices as $id => $commands) {
|
||||
$definition = $container->getDefinition($id);
|
||||
$class = $container->getParameterBag()->resolveValue($definition->getClass());
|
||||
|
||||
if (!$r = $container->getReflectionClass($class)) {
|
||||
throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
|
||||
}
|
||||
|
||||
foreach ($commands as $tags) {
|
||||
$this->registerCommand($container, $r, $id, $class, $tags, $definition, $serviceIds, $lazyCommandMap, $lazyCommandRefs);
|
||||
}
|
||||
}
|
||||
|
||||
$container
|
||||
->register('console.command_loader', ContainerCommandLoader::class)
|
||||
->setPublic(true)
|
||||
->addTag('container.no_preload')
|
||||
->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);
|
||||
|
||||
$container->setParameter('console.command.ids', $serviceIds);
|
||||
}
|
||||
|
||||
private function registerCommand(ContainerBuilder $container, \ReflectionClass $reflection, string $id, string $class, array $tags, Definition $definition, array &$serviceIds, array &$lazyCommandMap, array &$lazyCommandRefs): void
|
||||
{
|
||||
if (!$reflection->isSubclassOf(Command::class)) {
|
||||
$method = $tags[0]['method'] ?? '__invoke';
|
||||
|
||||
if (!$reflection->hasMethod($method)) {
|
||||
throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must either be a subclass of "%s" or have an "%s()" method.', $id, 'console.command', Command::class, $method));
|
||||
}
|
||||
|
||||
$reflection = $reflection->getMethod($method);
|
||||
|
||||
if (!$reflection->isPublic() || $reflection->isStatic()) {
|
||||
throw new InvalidArgumentException(\sprintf('The method "%s::%s()" must be public and non-static to be used as a console command.', $class, $method));
|
||||
}
|
||||
|
||||
if ('__invoke' === $method) {
|
||||
$callableRef = new Reference($id);
|
||||
$id .= '.command';
|
||||
} else {
|
||||
$callableRef = [new Reference($id), $method];
|
||||
$id .= '.'.$method.'.command';
|
||||
}
|
||||
$class = Command::class;
|
||||
|
||||
$closureDefinition = new Definition(\Closure::class)
|
||||
->setFactory([\Closure::class, 'fromCallable'])
|
||||
->setArguments([$callableRef]);
|
||||
|
||||
$definition = $container->register($id, $class)
|
||||
->addMethodCall('setCode', [$closureDefinition]);
|
||||
} elseif (isset($tags[0]['method'])) {
|
||||
throw new InvalidArgumentException(\sprintf('The service "%s" tagged "console.command" cannot define a method command when it is a subclass of "%s".', $id, Command::class));
|
||||
}
|
||||
|
||||
$definition->addTag('container.no_preload');
|
||||
|
||||
$attribute = $this->getCommandAttribute($reflection);
|
||||
$defaultName = $attribute?->name;
|
||||
$aliases = str_replace('%', '%%', $tags[0]['command'] ?? $defaultName ?? '');
|
||||
$aliases = explode('|', $aliases);
|
||||
$commandName = array_shift($aliases);
|
||||
|
||||
if ($isHidden = '' === $commandName) {
|
||||
$commandName = array_shift($aliases);
|
||||
}
|
||||
|
||||
if (null === $commandName) {
|
||||
if ($definition->isPrivate() || $definition->hasTag('container.private')) {
|
||||
$commandId = 'console.command.public_alias.'.$id;
|
||||
$container->setAlias($commandId, $id)->setPublic(true);
|
||||
$id = $commandId;
|
||||
}
|
||||
$serviceIds[] = $id;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$description = $tags[0]['description'] ?? null;
|
||||
$help = $tags[0]['help'] ?? null;
|
||||
$usages = $tags[0]['usages'] ?? null;
|
||||
|
||||
unset($tags[0]);
|
||||
$lazyCommandMap[$commandName] = $id;
|
||||
$lazyCommandRefs[$id] = new TypedReference($id, $class);
|
||||
|
||||
foreach ($aliases as $alias) {
|
||||
$lazyCommandMap[$alias] = $id;
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (isset($tag['command'])) {
|
||||
$aliases[] = $tag['command'];
|
||||
$lazyCommandMap[$tag['command']] = $id;
|
||||
}
|
||||
|
||||
$description ??= $tag['description'] ?? null;
|
||||
$help ??= $tag['help'] ?? null;
|
||||
$usages ??= $tag['usages'] ?? null;
|
||||
}
|
||||
|
||||
$definition->addMethodCall('setName', [$commandName]);
|
||||
|
||||
if ($aliases) {
|
||||
$definition->addMethodCall('setAliases', [$aliases]);
|
||||
}
|
||||
|
||||
if ($isHidden) {
|
||||
$definition->addMethodCall('setHidden', [true]);
|
||||
}
|
||||
|
||||
if ($help ??= $attribute?->help) {
|
||||
$definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]);
|
||||
}
|
||||
|
||||
if ($usages ??= $attribute?->usages) {
|
||||
foreach ($usages as $usage) {
|
||||
$definition->addMethodCall('addUsage', [$usage]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($description ??= $attribute?->description) {
|
||||
$escapedDescription = str_replace('%', '%%', $description);
|
||||
$definition->addMethodCall('setDescription', [$escapedDescription]);
|
||||
|
||||
$container->register('.'.$id.'.lazy', LazyCommand::class)
|
||||
->setArguments([$commandName, $aliases, $escapedDescription, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
|
||||
|
||||
$lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy');
|
||||
}
|
||||
}
|
||||
|
||||
private function getCommandAttribute(\ReflectionClass|\ReflectionMethod $reflection): ?AsCommand
|
||||
{
|
||||
/** @var AsCommand|null $attribute */
|
||||
if ($attribute = ($reflection->getAttributes(AsCommand::class)[0] ?? null)?->newInstance()) {
|
||||
return $attribute;
|
||||
}
|
||||
|
||||
if ($reflection instanceof \ReflectionMethod && '__invoke' === $reflection->getName()) {
|
||||
return ($reflection->getDeclaringClass()->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Console\ArgumentResolver\ValueResolver\TraceableValueResolver;
|
||||
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Gathers and configures the console argument value resolvers.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
class ConsoleArgumentValueResolverPass implements CompilerPassInterface
|
||||
{
|
||||
use PriorityTaggedServiceTrait;
|
||||
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition('console.argument_resolver')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definitions = $container->getDefinitions();
|
||||
$namedResolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('console.targeted_value_resolver', 'name', needsIndexes: true), $container);
|
||||
$resolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('console.argument_value_resolver', 'name', needsIndexes: true), $container);
|
||||
|
||||
foreach ($resolvers as $name => $resolver) {
|
||||
if ($definitions[(string) $resolver]->hasTag('console.targeted_value_resolver')) {
|
||||
unset($resolvers[$name]);
|
||||
} else {
|
||||
$namedResolvers[$name] ??= clone $resolver;
|
||||
}
|
||||
}
|
||||
|
||||
if ($container->getParameter('kernel.debug') && $container->has('debug.stopwatch')) {
|
||||
foreach ($resolvers as $name => $resolver) {
|
||||
$resolvers[$name] = new Reference('.debug.console.value_resolver.'.$resolver);
|
||||
$container->register('.debug.console.value_resolver.'.$resolver, TraceableValueResolver::class)
|
||||
->setArguments([$resolver, new Reference('debug.stopwatch')]);
|
||||
}
|
||||
foreach ($namedResolvers as $name => $resolver) {
|
||||
$namedResolvers[$name] = new Reference('.debug.console.value_resolver.'.$resolver);
|
||||
$container->register('.debug.console.value_resolver.'.$resolver, TraceableValueResolver::class)
|
||||
->setArguments([$resolver, new Reference('debug.stopwatch')]);
|
||||
}
|
||||
}
|
||||
|
||||
$container
|
||||
->getDefinition('console.argument_resolver')
|
||||
->replaceArgument(0, new IteratorArgument(array_values($resolvers)))
|
||||
->setArgument(1, new ServiceLocatorArgument($namedResolvers))
|
||||
;
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\RawInputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\TypedReference;
|
||||
use Symfony\Component\VarExporter\ProxyHelper;
|
||||
|
||||
/**
|
||||
* Creates the service-locators required by ServiceValueResolver for commands.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class RegisterCommandArgumentLocatorsPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition('console.argument_resolver.service')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parameterBag = $container->getParameterBag();
|
||||
$serviceLocators = [];
|
||||
|
||||
foreach ($container->findTaggedServiceIds('console.command.service_arguments', true) as $id => $tags) {
|
||||
$def = $container->getDefinition($id);
|
||||
$class = $def->getClass();
|
||||
$autowire = $def->isAutowired();
|
||||
$bindings = $def->getBindings();
|
||||
|
||||
// Resolve service class, taking parent definitions into account
|
||||
while ($def instanceof ChildDefinition) {
|
||||
$def = $container->findDefinition($def->getParent());
|
||||
$class = $class ?: $def->getClass();
|
||||
$bindings += $def->getBindings();
|
||||
}
|
||||
$class = $parameterBag->resolveValue($class);
|
||||
|
||||
if (!$r = $container->getReflectionClass($class)) {
|
||||
throw new InvalidArgumentException(\sprintf('Class "%s" used for command "%s" cannot be found.', $class, $id));
|
||||
}
|
||||
|
||||
// Get all console.command tags to find command names and their methods
|
||||
$commandTags = $container->getDefinition($id)->getTag('console.command');
|
||||
$manualArguments = [];
|
||||
|
||||
// Validate and collect explicit per-arguments service references
|
||||
foreach ($tags as $attributes) {
|
||||
if (!isset($attributes['argument']) && !isset($attributes['id'])) {
|
||||
$autowire = true;
|
||||
continue;
|
||||
}
|
||||
foreach (['argument', 'id'] as $k) {
|
||||
if (!isset($attributes[$k][0])) {
|
||||
throw new InvalidArgumentException(\sprintf('Missing "%s" attribute on tag "console.command.service_arguments" %s for service "%s".', $k, json_encode($attributes, \JSON_UNESCAPED_UNICODE), $id));
|
||||
}
|
||||
}
|
||||
|
||||
$manualArguments[$attributes['argument']] = $attributes['id'];
|
||||
}
|
||||
|
||||
foreach ($commandTags as $commandTag) {
|
||||
$commandName = $commandTag['command'] ?? null;
|
||||
|
||||
if (!$commandName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$methodName = $commandTag['method'] ?? '__invoke';
|
||||
|
||||
if (!$r->hasMethod($methodName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = $r->getMethod($methodName);
|
||||
$arguments = [];
|
||||
$erroredIds = 0;
|
||||
|
||||
foreach ($method->getParameters() as $p) {
|
||||
$type = preg_replace('/(^|[(|&])\\\\/', '\1', $target = ltrim(ProxyHelper::exportType($p) ?? '', '?'));
|
||||
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
|
||||
$autowireAttributes = null;
|
||||
$parsedName = $p->name;
|
||||
$k = null;
|
||||
|
||||
if (isset($manualArguments[$p->name])) {
|
||||
$target = $manualArguments[$p->name];
|
||||
if ('?' !== $target[0]) {
|
||||
$invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE;
|
||||
} elseif ('' === $target = substr($target, 1)) {
|
||||
throw new InvalidArgumentException(\sprintf('A "console.command.service_arguments" tag must have non-empty "id" attributes for service "%s".', $id));
|
||||
} elseif ($p->allowsNull() && !$p->isOptional()) {
|
||||
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
|
||||
}
|
||||
} elseif (isset($bindings[$bindingName = $type.' $'.$name = Target::parseName($p, $k, $parsedName)])
|
||||
|| isset($bindings[$bindingName = $type.' $'.$parsedName])
|
||||
|| isset($bindings[$bindingName = '$'.$name])
|
||||
|| isset($bindings[$bindingName = $type])
|
||||
) {
|
||||
$binding = $bindings[$bindingName];
|
||||
|
||||
[$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues();
|
||||
$binding->setValues([$bindingValue, $bindingId, true, $bindingType, $bindingFile]);
|
||||
|
||||
$arguments[$p->name] = $bindingValue;
|
||||
|
||||
continue;
|
||||
} elseif (!$autowire || (!($autowireAttributes = $p->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF)) && (!$type || '\\' !== $target[0]))) {
|
||||
continue;
|
||||
} elseif (!$autowireAttributes && is_subclass_of($type, \UnitEnum::class)) {
|
||||
// Do not attempt to register enum typed arguments if not already present in bindings
|
||||
continue;
|
||||
} elseif (!$p->allowsNull()) {
|
||||
$invalidBehavior = $autowireAttributes ? ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE : ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE;
|
||||
}
|
||||
|
||||
// Skip console-specific types that are resolved by other resolvers
|
||||
if (\in_array($type, [InputInterface::class, RawInputInterface::class, OutputInterface::class], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($autowireAttributes) {
|
||||
$attribute = $autowireAttributes[0]->newInstance();
|
||||
$value = $parameterBag->resolveValue($attribute->value);
|
||||
|
||||
if ($attribute instanceof AutowireCallable) {
|
||||
$arguments[$p->name] = $attribute->buildDefinition($value, $type, $p);
|
||||
} elseif ($value instanceof Reference) {
|
||||
$arguments[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior);
|
||||
} else {
|
||||
$arguments[$p->name] = new Reference('.value.'.$container->hash($value));
|
||||
$container->register((string) $arguments[$p->name], 'mixed')
|
||||
->setFactory('current')
|
||||
->addArgument([$value]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type && !$p->isOptional() && !$p->allowsNull() && !class_exists($type) && !interface_exists($type, false)) {
|
||||
$message = \sprintf('Cannot determine command argument for "%s::%s()": the $%s argument is type-hinted with the non-existent class or interface: "%s".', $class, $method->name, $p->name, $type);
|
||||
|
||||
// See if the type-hint lives in the same namespace as the command
|
||||
if (0 === strncmp($type, $class, strrpos($class, '\\'))) {
|
||||
$message .= ' Did you forget to add a use statement?';
|
||||
}
|
||||
|
||||
$container->register($erroredId = '.errored.'.$container->hash($message), $type)
|
||||
->addError($message);
|
||||
|
||||
$arguments[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE);
|
||||
++$erroredIds;
|
||||
} else {
|
||||
$target = preg_replace('/(^|[(|&])\\\\/', '\1', $target);
|
||||
$arguments[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, Target::parseName($p)) : new Reference($target, $invalidBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
if ($arguments) {
|
||||
$serviceLocators[$commandName] = ServiceLocatorTagPass::register($container, $arguments, \count($arguments) !== $erroredIds ? $commandName : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$container->getDefinition('console.argument_resolver.service')
|
||||
->replaceArgument(0, ServiceLocatorTagPass::register($container, $serviceLocators));
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* Removes empty service-locators registered for ServiceValueResolver for commands.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class RemoveEmptyCommandArgumentLocatorsPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition('console.argument_resolver.service')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$serviceResolverDef = $container->getDefinition('console.argument_resolver.service');
|
||||
$commandLocatorRef = $serviceResolverDef->getArgument(0);
|
||||
|
||||
if (!$commandLocatorRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commandLocator = $container->getDefinition((string) $commandLocatorRef);
|
||||
|
||||
if ($commandLocator->getFactory()) {
|
||||
$commandLocator = $container->getDefinition($commandLocator->getFactory()[0]);
|
||||
}
|
||||
|
||||
$commands = $commandLocator->getArgument(0);
|
||||
|
||||
foreach ($commands as $commandName => $argumentRef) {
|
||||
$argumentLocator = $container->getDefinition((string) $argumentRef->getValues()[0]);
|
||||
|
||||
if ($argumentLocator->getFactory()) {
|
||||
$argumentLocator = $container->getDefinition($argumentLocator->getFactory()[0]);
|
||||
}
|
||||
|
||||
if (!$argumentLocator->getArgument(0)) {
|
||||
$reason = \sprintf('Removing service-argument resolver for command "%s": no corresponding services exist for the referenced types.', $commandName);
|
||||
unset($commands[$commandName]);
|
||||
$container->log($this, $reason);
|
||||
}
|
||||
}
|
||||
|
||||
$commandLocator->replaceArgument(0, $commands);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
|
||||
/**
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ApplicationDescription
|
||||
{
|
||||
public const GLOBAL_NAMESPACE = '_global';
|
||||
|
||||
private array $namespaces;
|
||||
|
||||
/**
|
||||
* @var array<string, Command>
|
||||
*/
|
||||
private array $commands;
|
||||
|
||||
/**
|
||||
* @var array<string, Command>
|
||||
*/
|
||||
private array $aliases = [];
|
||||
|
||||
public function __construct(
|
||||
private Application $application,
|
||||
private ?string $namespace = null,
|
||||
private bool $showHidden = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getNamespaces(): array
|
||||
{
|
||||
if (!isset($this->namespaces)) {
|
||||
$this->inspectApplication();
|
||||
}
|
||||
|
||||
return $this->namespaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Command[]
|
||||
*/
|
||||
public function getCommands(): array
|
||||
{
|
||||
if (!isset($this->commands)) {
|
||||
$this->inspectApplication();
|
||||
}
|
||||
|
||||
return $this->commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CommandNotFoundException
|
||||
*/
|
||||
public function getCommand(string $name): Command
|
||||
{
|
||||
if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
|
||||
throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
return $this->commands[$name] ?? $this->aliases[$name];
|
||||
}
|
||||
|
||||
private function inspectApplication(): void
|
||||
{
|
||||
$this->commands = [];
|
||||
$this->namespaces = [];
|
||||
|
||||
$all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null);
|
||||
foreach ($this->sortCommands($all) as $namespace => $commands) {
|
||||
$names = [];
|
||||
|
||||
foreach ($commands as $name => $command) {
|
||||
if (!$command->getName() || (!$this->showHidden && $command->isHidden())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($command->getName() === $name) {
|
||||
$this->commands[$name] = $command;
|
||||
} else {
|
||||
$this->aliases[$name] = $command;
|
||||
}
|
||||
|
||||
$names[] = $name;
|
||||
}
|
||||
|
||||
$this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, Command>>
|
||||
*/
|
||||
private function sortCommands(array $commands): array
|
||||
{
|
||||
$namespacedCommands = [];
|
||||
$globalCommands = [];
|
||||
$sortedCommands = [];
|
||||
foreach ($commands as $name => $command) {
|
||||
$key = $this->application->extractNamespace($name, 1);
|
||||
if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) {
|
||||
$globalCommands[$name] = $command;
|
||||
} else {
|
||||
$namespacedCommands[$key][$name] = $command;
|
||||
}
|
||||
}
|
||||
|
||||
if ($globalCommands) {
|
||||
ksort($globalCommands);
|
||||
$sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands;
|
||||
}
|
||||
|
||||
if ($namespacedCommands) {
|
||||
ksort($namespacedCommands, \SORT_STRING);
|
||||
foreach ($namespacedCommands as $key => $commandsSet) {
|
||||
ksort($commandsSet);
|
||||
$sortedCommands[$key] = $commandsSet;
|
||||
}
|
||||
}
|
||||
|
||||
return $sortedCommands;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class Descriptor implements DescriptorInterface
|
||||
{
|
||||
protected OutputInterface $output;
|
||||
|
||||
public function describe(OutputInterface $output, object $object, array $options = []): void
|
||||
{
|
||||
$this->output = $output;
|
||||
|
||||
match (true) {
|
||||
$object instanceof InputArgument => $this->describeInputArgument($object, $options),
|
||||
$object instanceof InputOption => $this->describeInputOption($object, $options),
|
||||
$object instanceof InputDefinition => $this->describeInputDefinition($object, $options),
|
||||
$object instanceof Command => $this->describeCommand($object, $options),
|
||||
$object instanceof Application => $this->describeApplication($object, $options),
|
||||
default => throw new InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
|
||||
};
|
||||
}
|
||||
|
||||
protected function write(string $content, bool $decorated = false): void
|
||||
{
|
||||
$this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an InputArgument instance.
|
||||
*/
|
||||
abstract protected function describeInputArgument(InputArgument $argument, array $options = []): void;
|
||||
|
||||
/**
|
||||
* Describes an InputOption instance.
|
||||
*/
|
||||
abstract protected function describeInputOption(InputOption $option, array $options = []): void;
|
||||
|
||||
/**
|
||||
* Describes an InputDefinition instance.
|
||||
*/
|
||||
abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []): void;
|
||||
|
||||
/**
|
||||
* Describes a Command instance.
|
||||
*/
|
||||
abstract protected function describeCommand(Command $command, array $options = []): void;
|
||||
|
||||
/**
|
||||
* Describes an Application instance.
|
||||
*/
|
||||
abstract protected function describeApplication(Application $application, array $options = []): void;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Descriptor interface.
|
||||
*
|
||||
* @author Jean-François Simon <contact@jfsimon.fr>
|
||||
*/
|
||||
interface DescriptorInterface
|
||||
{
|
||||
public function describe(OutputInterface $output, object $object, array $options = []): void;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* JSON descriptor.
|
||||
*
|
||||
* @author Jean-François Simon <contact@jfsimon.fr>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonDescriptor extends Descriptor
|
||||
{
|
||||
protected function describeInputArgument(InputArgument $argument, array $options = []): void
|
||||
{
|
||||
$this->writeData($this->getInputArgumentData($argument), $options);
|
||||
}
|
||||
|
||||
protected function describeInputOption(InputOption $option, array $options = []): void
|
||||
{
|
||||
$this->writeData($this->getInputOptionData($option), $options);
|
||||
if ($option->isNegatable()) {
|
||||
$this->writeData($this->getInputOptionData($option, true), $options);
|
||||
}
|
||||
}
|
||||
|
||||
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
|
||||
{
|
||||
$this->writeData($this->getInputDefinitionData($definition), $options);
|
||||
}
|
||||
|
||||
protected function describeCommand(Command $command, array $options = []): void
|
||||
{
|
||||
$this->writeData($this->getCommandData($command, $options['short'] ?? false), $options);
|
||||
}
|
||||
|
||||
protected function describeApplication(Application $application, array $options = []): void
|
||||
{
|
||||
$describedNamespace = $options['namespace'] ?? null;
|
||||
$description = new ApplicationDescription($application, $describedNamespace, true);
|
||||
$commands = [];
|
||||
|
||||
foreach ($description->getCommands() as $command) {
|
||||
$commands[] = $this->getCommandData($command, $options['short'] ?? false);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
if ('UNKNOWN' !== $application->getName()) {
|
||||
$data['application']['name'] = $application->getName();
|
||||
if ('UNKNOWN' !== $application->getVersion()) {
|
||||
$data['application']['version'] = $application->getVersion();
|
||||
}
|
||||
}
|
||||
|
||||
$data['commands'] = $commands;
|
||||
|
||||
if ($describedNamespace) {
|
||||
$data['namespace'] = $describedNamespace;
|
||||
} else {
|
||||
$data['namespaces'] = array_values($description->getNamespaces());
|
||||
}
|
||||
|
||||
$this->writeData($data, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes data as json.
|
||||
*/
|
||||
private function writeData(array $data, array $options): void
|
||||
{
|
||||
$flags = $options['json_encoding'] ?? 0;
|
||||
|
||||
$this->write(json_encode($data, $flags));
|
||||
}
|
||||
|
||||
private function getInputArgumentData(InputArgument $argument): array
|
||||
{
|
||||
return [
|
||||
'name' => $argument->getName(),
|
||||
'is_required' => $argument->isRequired(),
|
||||
'is_array' => $argument->isArray(),
|
||||
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()),
|
||||
'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getInputOptionData(InputOption $option, bool $negated = false): array
|
||||
{
|
||||
return $negated ? [
|
||||
'name' => '--no-'.$option->getName(),
|
||||
'shortcut' => '',
|
||||
'accept_value' => false,
|
||||
'is_value_required' => false,
|
||||
'is_multiple' => false,
|
||||
'description' => 'Negate the "--'.$option->getName().'" option',
|
||||
'default' => null === $option->getDefault() ? null : !$option->getDefault(),
|
||||
] : [
|
||||
'name' => '--'.$option->getName(),
|
||||
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
|
||||
'accept_value' => $option->acceptValue(),
|
||||
'is_value_required' => $option->isValueRequired(),
|
||||
'is_multiple' => $option->isArray(),
|
||||
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()),
|
||||
'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getInputDefinitionData(InputDefinition $definition): array
|
||||
{
|
||||
$inputArguments = [];
|
||||
foreach ($definition->getArguments() as $name => $argument) {
|
||||
$inputArguments[$name] = $this->getInputArgumentData($argument);
|
||||
}
|
||||
|
||||
$inputOptions = [];
|
||||
foreach ($definition->getOptions() as $name => $option) {
|
||||
$inputOptions[$name] = $this->getInputOptionData($option);
|
||||
if ($option->isNegatable()) {
|
||||
$inputOptions['no-'.$name] = $this->getInputOptionData($option, true);
|
||||
}
|
||||
}
|
||||
|
||||
return ['arguments' => $inputArguments, 'options' => $inputOptions];
|
||||
}
|
||||
|
||||
private function getCommandData(Command $command, bool $short = false): array
|
||||
{
|
||||
$data = [
|
||||
'name' => $command->getName(),
|
||||
'description' => $command->getDescription(),
|
||||
];
|
||||
|
||||
if ($short) {
|
||||
$data += [
|
||||
'usage' => $command->getAliases(),
|
||||
];
|
||||
} else {
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
$data += [
|
||||
'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
|
||||
'help' => $command->getProcessedHelp(),
|
||||
'definition' => $this->getInputDefinitionData($command->getDefinition()),
|
||||
];
|
||||
}
|
||||
|
||||
$data['hidden'] = $command->isHidden();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Helper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Markdown descriptor.
|
||||
*
|
||||
* @author Jean-François Simon <contact@jfsimon.fr>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MarkdownDescriptor extends Descriptor
|
||||
{
|
||||
public function describe(OutputInterface $output, object $object, array $options = []): void
|
||||
{
|
||||
$decorated = $output->isDecorated();
|
||||
$output->setDecorated(false);
|
||||
|
||||
parent::describe($output, $object, $options);
|
||||
|
||||
$output->setDecorated($decorated);
|
||||
}
|
||||
|
||||
protected function write(string $content, bool $decorated = true): void
|
||||
{
|
||||
parent::write($content, $decorated);
|
||||
}
|
||||
|
||||
protected function describeInputArgument(InputArgument $argument, array $options = []): void
|
||||
{
|
||||
$this->write(
|
||||
'#### `'.($argument->getName() ?: '<none>')."`\n\n"
|
||||
.($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
|
||||
.'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n"
|
||||
.'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n"
|
||||
.'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`'
|
||||
);
|
||||
}
|
||||
|
||||
protected function describeInputOption(InputOption $option, array $options = []): void
|
||||
{
|
||||
$name = '--'.$option->getName();
|
||||
if ($option->isNegatable()) {
|
||||
$name .= '|--no-'.$option->getName();
|
||||
}
|
||||
if ($option->getShortcut()) {
|
||||
$name .= '|-'.str_replace('|', '|-', $option->getShortcut()).'';
|
||||
}
|
||||
|
||||
$this->write(
|
||||
'#### `'.$name.'`'."\n\n"
|
||||
.($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '')
|
||||
.'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n"
|
||||
.'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
|
||||
.'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n"
|
||||
.'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n"
|
||||
.'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
|
||||
);
|
||||
}
|
||||
|
||||
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
|
||||
{
|
||||
if ($showArguments = \count($definition->getArguments()) > 0) {
|
||||
$this->write('### Arguments');
|
||||
foreach ($definition->getArguments() as $argument) {
|
||||
$this->write("\n\n");
|
||||
$this->describeInputArgument($argument);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($definition->getOptions()) > 0) {
|
||||
if ($showArguments) {
|
||||
$this->write("\n\n");
|
||||
}
|
||||
|
||||
$this->write('### Options');
|
||||
foreach ($definition->getOptions() as $option) {
|
||||
$this->write("\n\n");
|
||||
$this->describeInputOption($option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function describeCommand(Command $command, array $options = []): void
|
||||
{
|
||||
if ($options['short'] ?? false) {
|
||||
$this->write(
|
||||
'`'.$command->getName()."`\n"
|
||||
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
|
||||
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
|
||||
.'### Usage'."\n\n"
|
||||
.array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
$this->write(
|
||||
'`'.$command->getName()."`\n"
|
||||
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
|
||||
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
|
||||
.'### Usage'."\n\n"
|
||||
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
|
||||
);
|
||||
|
||||
if ($help = $command->getProcessedHelp()) {
|
||||
$this->write("\n");
|
||||
$this->write($help);
|
||||
}
|
||||
|
||||
$definition = $command->getDefinition();
|
||||
if ($definition->getOptions() || $definition->getArguments()) {
|
||||
$this->write("\n\n");
|
||||
$this->describeInputDefinition($definition);
|
||||
}
|
||||
}
|
||||
|
||||
protected function describeApplication(Application $application, array $options = []): void
|
||||
{
|
||||
$describedNamespace = $options['namespace'] ?? null;
|
||||
$description = new ApplicationDescription($application, $describedNamespace);
|
||||
$title = $this->getApplicationTitle($application);
|
||||
|
||||
$this->write($title."\n".str_repeat('=', Helper::width($title)));
|
||||
|
||||
foreach ($description->getNamespaces() as $namespace) {
|
||||
if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
|
||||
$this->write("\n\n");
|
||||
$this->write('**'.$namespace['id'].':**');
|
||||
}
|
||||
|
||||
$this->write("\n\n");
|
||||
$this->write(implode("\n", array_map(static fn ($commandName) => \sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())), $namespace['commands'])));
|
||||
}
|
||||
|
||||
foreach ($description->getCommands() as $command) {
|
||||
$this->write("\n\n");
|
||||
$this->describeCommand($command, $options);
|
||||
}
|
||||
}
|
||||
|
||||
private function getApplicationTitle(Application $application): string
|
||||
{
|
||||
if ('UNKNOWN' !== $application->getName()) {
|
||||
if ('UNKNOWN' !== $application->getVersion()) {
|
||||
return \sprintf('%s %s', $application->getName(), $application->getVersion());
|
||||
}
|
||||
|
||||
return $application->getName();
|
||||
}
|
||||
|
||||
return 'Console Tool';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Helper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
class ReStructuredTextDescriptor extends Descriptor
|
||||
{
|
||||
// <h1>
|
||||
private string $partChar = '=';
|
||||
// <h2>
|
||||
private string $chapterChar = '-';
|
||||
// <h3>
|
||||
private string $sectionChar = '~';
|
||||
// <h4>
|
||||
private string $subsectionChar = '.';
|
||||
// <h5>
|
||||
private string $subsubsectionChar = '^';
|
||||
// <h6>
|
||||
private string $paragraphsChar = '"';
|
||||
|
||||
private array $visibleNamespaces = [];
|
||||
|
||||
public function describe(OutputInterface $output, object $object, array $options = []): void
|
||||
{
|
||||
$decorated = $output->isDecorated();
|
||||
$output->setDecorated(false);
|
||||
|
||||
parent::describe($output, $object, $options);
|
||||
|
||||
$output->setDecorated($decorated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override parent method to set $decorated = true.
|
||||
*/
|
||||
protected function write(string $content, bool $decorated = true): void
|
||||
{
|
||||
parent::write($content, $decorated);
|
||||
}
|
||||
|
||||
protected function describeInputArgument(InputArgument $argument, array $options = []): void
|
||||
{
|
||||
$this->write(
|
||||
$argument->getName() ?: '<none>'."\n".str_repeat($this->paragraphsChar, Helper::width($argument->getName()))."\n\n"
|
||||
.($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
|
||||
.'- **Is required**: '.($argument->isRequired() ? 'yes' : 'no')."\n"
|
||||
.'- **Is array**: '.($argument->isArray() ? 'yes' : 'no')."\n"
|
||||
.'- **Default**: ``'.str_replace("\n", '', var_export($argument->getDefault(), true)).'``'
|
||||
);
|
||||
}
|
||||
|
||||
protected function describeInputOption(InputOption $option, array $options = []): void
|
||||
{
|
||||
$name = '\-\-'.$option->getName();
|
||||
if ($option->isNegatable()) {
|
||||
$name .= '|\-\-no-'.$option->getName();
|
||||
}
|
||||
if ($option->getShortcut()) {
|
||||
$name .= '|-'.str_replace('|', '|-', $option->getShortcut());
|
||||
}
|
||||
|
||||
$optionDescription = $option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n\n", $option->getDescription())."\n\n" : '';
|
||||
$optionDescription = (new UnicodeString($optionDescription))->ascii();
|
||||
$this->write(
|
||||
$name."\n".str_repeat($this->paragraphsChar, Helper::width($name))."\n\n"
|
||||
.$optionDescription
|
||||
.'- **Accept value**: '.($option->acceptValue() ? 'yes' : 'no')."\n"
|
||||
.'- **Is value required**: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
|
||||
.'- **Is multiple**: '.($option->isArray() ? 'yes' : 'no')."\n"
|
||||
.'- **Is negatable**: '.($option->isNegatable() ? 'yes' : 'no')."\n"
|
||||
.'- **Default**: ``'.str_replace("\n", '', var_export($option->getDefault(), true)).'``'."\n"
|
||||
);
|
||||
}
|
||||
|
||||
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
|
||||
{
|
||||
if ($showArguments = ((bool) $definition->getArguments())) {
|
||||
$this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9));
|
||||
foreach ($definition->getArguments() as $argument) {
|
||||
$this->write("\n\n");
|
||||
$this->describeInputArgument($argument);
|
||||
}
|
||||
}
|
||||
|
||||
if ($nonDefaultOptions = $this->getNonDefaultOptions($definition)) {
|
||||
if ($showArguments) {
|
||||
$this->write("\n\n");
|
||||
}
|
||||
|
||||
$this->write("Options\n".str_repeat($this->subsubsectionChar, 7)."\n\n");
|
||||
foreach ($nonDefaultOptions as $option) {
|
||||
$this->describeInputOption($option);
|
||||
$this->write("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function describeCommand(Command $command, array $options = []): void
|
||||
{
|
||||
if ($options['short'] ?? false) {
|
||||
$this->write(
|
||||
'``'.$command->getName()."``\n"
|
||||
.str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
|
||||
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
|
||||
."Usage\n".str_repeat($this->paragraphsChar, 5)."\n\n"
|
||||
.array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
foreach ($command->getAliases() as $alias) {
|
||||
$this->write('.. _'.$alias.":\n\n");
|
||||
}
|
||||
$this->write(
|
||||
$command->getName()."\n"
|
||||
.str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
|
||||
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
|
||||
."Usage\n".str_repeat($this->subsubsectionChar, 5)."\n\n"
|
||||
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
|
||||
);
|
||||
|
||||
if ($help = $command->getProcessedHelp()) {
|
||||
$this->write("\n");
|
||||
$this->write($help);
|
||||
}
|
||||
|
||||
$definition = $command->getDefinition();
|
||||
if ($definition->getOptions() || $definition->getArguments()) {
|
||||
$this->write("\n\n");
|
||||
$this->describeInputDefinition($definition);
|
||||
}
|
||||
}
|
||||
|
||||
protected function describeApplication(Application $application, array $options = []): void
|
||||
{
|
||||
$description = new ApplicationDescription($application, $options['namespace'] ?? null);
|
||||
$title = $this->getApplicationTitle($application);
|
||||
|
||||
$this->write($title."\n".str_repeat($this->partChar, Helper::width($title)));
|
||||
$this->createTableOfContents($description, $application);
|
||||
$this->describeCommands($application, $options);
|
||||
}
|
||||
|
||||
private function getApplicationTitle(Application $application): string
|
||||
{
|
||||
if ('UNKNOWN' === $application->getName()) {
|
||||
return 'Console Tool';
|
||||
}
|
||||
if ('UNKNOWN' !== $application->getVersion()) {
|
||||
return \sprintf('%s %s', $application->getName(), $application->getVersion());
|
||||
}
|
||||
|
||||
return $application->getName();
|
||||
}
|
||||
|
||||
private function describeCommands($application, array $options): void
|
||||
{
|
||||
$title = 'Commands';
|
||||
$this->write("\n\n$title\n".str_repeat($this->chapterChar, Helper::width($title))."\n\n");
|
||||
foreach ($this->visibleNamespaces as $namespace) {
|
||||
if ('_global' === $namespace) {
|
||||
$commands = $application->all('');
|
||||
$this->write('Global'."\n".str_repeat($this->sectionChar, Helper::width('Global'))."\n\n");
|
||||
} else {
|
||||
$commands = $application->all($namespace);
|
||||
$this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
|
||||
}
|
||||
|
||||
foreach ($this->removeAliasesAndHiddenCommands($commands) as $command) {
|
||||
$this->describeCommand($command, $options);
|
||||
$this->write("\n\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createTableOfContents(ApplicationDescription $description, Application $application): void
|
||||
{
|
||||
$this->setVisibleNamespaces($description);
|
||||
$chapterTitle = 'Table of Contents';
|
||||
$this->write("\n\n$chapterTitle\n".str_repeat($this->chapterChar, Helper::width($chapterTitle))."\n\n");
|
||||
foreach ($this->visibleNamespaces as $namespace) {
|
||||
if ('_global' === $namespace) {
|
||||
$commands = $application->all('');
|
||||
} else {
|
||||
$commands = $application->all($namespace);
|
||||
$this->write("\n\n");
|
||||
$this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
|
||||
}
|
||||
$commands = $this->removeAliasesAndHiddenCommands($commands);
|
||||
|
||||
$this->write("\n\n");
|
||||
$this->write(implode("\n", array_map(static fn ($commandName) => \sprintf('- `%s`_', $commandName), array_keys($commands))));
|
||||
}
|
||||
}
|
||||
|
||||
private function getNonDefaultOptions(InputDefinition $definition): array
|
||||
{
|
||||
$globalOptions = [
|
||||
'help',
|
||||
'silent',
|
||||
'quiet',
|
||||
'verbose',
|
||||
'version',
|
||||
'ansi',
|
||||
'no-interaction',
|
||||
];
|
||||
$nonDefaultOptions = [];
|
||||
foreach ($definition->getOptions() as $option) {
|
||||
// Skip global options.
|
||||
if (!\in_array($option->getName(), $globalOptions, true)) {
|
||||
$nonDefaultOptions[] = $option;
|
||||
}
|
||||
}
|
||||
|
||||
return $nonDefaultOptions;
|
||||
}
|
||||
|
||||
private function setVisibleNamespaces(ApplicationDescription $description): void
|
||||
{
|
||||
$commands = $description->getCommands();
|
||||
foreach ($description->getNamespaces() as $namespace) {
|
||||
try {
|
||||
$namespaceCommands = $namespace['commands'];
|
||||
foreach ($namespaceCommands as $key => $commandName) {
|
||||
if (!\array_key_exists($commandName, $commands)) {
|
||||
// If the array key does not exist, then this is an alias.
|
||||
unset($namespaceCommands[$key]);
|
||||
} elseif ($commands[$commandName]->isHidden()) {
|
||||
unset($namespaceCommands[$key]);
|
||||
}
|
||||
}
|
||||
if (!$namespaceCommands) {
|
||||
// If the namespace contained only aliases or hidden commands, skip the namespace.
|
||||
continue;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
}
|
||||
$this->visibleNamespaces[] = $namespace['id'];
|
||||
}
|
||||
}
|
||||
|
||||
private function removeAliasesAndHiddenCommands(array $commands): array
|
||||
{
|
||||
foreach ($commands as $key => $command) {
|
||||
if ($command->isHidden() || \in_array($key, $command->getAliases(), true)) {
|
||||
unset($commands[$key]);
|
||||
}
|
||||
}
|
||||
unset($commands['completion']);
|
||||
|
||||
return $commands;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Helper\Helper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Text descriptor.
|
||||
*
|
||||
* @author Jean-François Simon <contact@jfsimon.fr>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TextDescriptor extends Descriptor
|
||||
{
|
||||
protected function describeInputArgument(InputArgument $argument, array $options = []): void
|
||||
{
|
||||
if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) {
|
||||
$default = \sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$totalWidth = $options['total_width'] ?? Helper::width($argument->getName());
|
||||
$spacingWidth = $totalWidth - \strlen($argument->getName());
|
||||
|
||||
$this->writeText(\sprintf(' <info>%s</info> %s%s%s',
|
||||
$argument->getName(),
|
||||
str_repeat(' ', $spacingWidth),
|
||||
// + 4 = 2 spaces before <info>, 2 spaces after </info>
|
||||
preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()),
|
||||
$default
|
||||
), $options);
|
||||
}
|
||||
|
||||
protected function describeInputOption(InputOption $option, array $options = []): void
|
||||
{
|
||||
if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) {
|
||||
$default = \sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$value = '';
|
||||
if ($option->acceptValue()) {
|
||||
$value = '='.strtoupper($option->getName());
|
||||
|
||||
if ($option->isValueOptional()) {
|
||||
$value = '['.$value.']';
|
||||
}
|
||||
}
|
||||
|
||||
$totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
|
||||
$synopsis = \sprintf('%s%s',
|
||||
$option->getShortcut() ? \sprintf('-%s, ', $option->getShortcut()) : ' ',
|
||||
\sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
|
||||
);
|
||||
|
||||
$spacingWidth = $totalWidth - Helper::width($synopsis);
|
||||
|
||||
$this->writeText(\sprintf(' <info>%s</info> %s%s%s%s',
|
||||
$synopsis,
|
||||
str_repeat(' ', $spacingWidth),
|
||||
// + 4 = 2 spaces before <info>, 2 spaces after </info>
|
||||
preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()),
|
||||
$default,
|
||||
$option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''
|
||||
), $options);
|
||||
}
|
||||
|
||||
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
|
||||
{
|
||||
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
|
||||
foreach ($definition->getArguments() as $argument) {
|
||||
$totalWidth = max($totalWidth, Helper::width($argument->getName()));
|
||||
}
|
||||
|
||||
if ($definition->getArguments()) {
|
||||
$this->writeText('<comment>Arguments:</comment>', $options);
|
||||
$this->writeText("\n");
|
||||
foreach ($definition->getArguments() as $argument) {
|
||||
$this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
|
||||
$this->writeText("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if ($definition->getArguments() && $definition->getOptions()) {
|
||||
$this->writeText("\n");
|
||||
}
|
||||
|
||||
if ($definition->getOptions()) {
|
||||
$laterOptions = [];
|
||||
|
||||
$this->writeText('<comment>Options:</comment>', $options);
|
||||
foreach ($definition->getOptions() as $option) {
|
||||
if (\strlen($option->getShortcut() ?? '') > 1) {
|
||||
$laterOptions[] = $option;
|
||||
continue;
|
||||
}
|
||||
$this->writeText("\n");
|
||||
$this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
|
||||
}
|
||||
foreach ($laterOptions as $option) {
|
||||
$this->writeText("\n");
|
||||
$this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function describeCommand(Command $command, array $options = []): void
|
||||
{
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
if ($description = $command->getDescription()) {
|
||||
$this->writeText('<comment>Description:</comment>', $options);
|
||||
$this->writeText("\n");
|
||||
$this->writeText(' '.$description);
|
||||
$this->writeText("\n\n");
|
||||
}
|
||||
|
||||
$this->writeText('<comment>Usage:</comment>', $options);
|
||||
foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
|
||||
$this->writeText("\n");
|
||||
$this->writeText(' '.OutputFormatter::escape($usage), $options);
|
||||
}
|
||||
$this->writeText("\n");
|
||||
|
||||
$definition = $command->getDefinition();
|
||||
if ($definition->getOptions() || $definition->getArguments()) {
|
||||
$this->writeText("\n");
|
||||
$this->describeInputDefinition($definition, $options);
|
||||
$this->writeText("\n");
|
||||
}
|
||||
|
||||
$help = $command->getProcessedHelp();
|
||||
if ($help && $help !== $description) {
|
||||
$this->writeText("\n");
|
||||
$this->writeText('<comment>Help:</comment>', $options);
|
||||
$this->writeText("\n");
|
||||
$this->writeText(' '.str_replace("\n", "\n ", $help), $options);
|
||||
$this->writeText("\n");
|
||||
}
|
||||
}
|
||||
|
||||
protected function describeApplication(Application $application, array $options = []): void
|
||||
{
|
||||
$describedNamespace = $options['namespace'] ?? null;
|
||||
$description = new ApplicationDescription($application, $describedNamespace);
|
||||
|
||||
if (isset($options['raw_text']) && $options['raw_text']) {
|
||||
$width = $this->getColumnWidth($description->getCommands());
|
||||
|
||||
foreach ($description->getCommands() as $command) {
|
||||
$this->writeText(\sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
|
||||
$this->writeText("\n");
|
||||
}
|
||||
} else {
|
||||
if ('' != $help = $application->getHelp()) {
|
||||
$this->writeText("$help\n\n", $options);
|
||||
}
|
||||
|
||||
$this->writeText("<comment>Usage:</comment>\n", $options);
|
||||
$this->writeText(" command [options] [arguments]\n\n", $options);
|
||||
|
||||
$this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options);
|
||||
|
||||
$this->writeText("\n");
|
||||
$this->writeText("\n");
|
||||
|
||||
$commands = $description->getCommands();
|
||||
$namespaces = $description->getNamespaces();
|
||||
if ($describedNamespace && $namespaces) {
|
||||
// make sure all alias commands are included when describing a specific namespace
|
||||
$describedNamespaceInfo = reset($namespaces);
|
||||
foreach ($describedNamespaceInfo['commands'] as $name) {
|
||||
$commands[$name] = $description->getCommand($name);
|
||||
}
|
||||
}
|
||||
|
||||
// calculate max. width based on available commands per namespace
|
||||
$width = $this->getColumnWidth(array_merge(...array_values(array_map(static fn ($namespace) => array_intersect($namespace['commands'], array_keys($commands)), array_values($namespaces)))));
|
||||
|
||||
if ($describedNamespace) {
|
||||
$this->writeText(\sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
|
||||
} else {
|
||||
$this->writeText('<comment>Available commands:</comment>', $options);
|
||||
}
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
$namespace['commands'] = array_filter($namespace['commands'], static fn ($name) => isset($commands[$name]));
|
||||
|
||||
if (!$namespace['commands']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
|
||||
$this->writeText("\n");
|
||||
$this->writeText(' <comment>'.$namespace['id'].'</comment>', $options);
|
||||
}
|
||||
|
||||
foreach ($namespace['commands'] as $name) {
|
||||
$this->writeText("\n");
|
||||
$spacingWidth = $width - Helper::width($name);
|
||||
$command = $commands[$name];
|
||||
$commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : '';
|
||||
$this->writeText(\sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeText("\n");
|
||||
}
|
||||
}
|
||||
|
||||
private function writeText(string $content, array $options = []): void
|
||||
{
|
||||
$this->write(
|
||||
isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
|
||||
isset($options['raw_output']) ? !$options['raw_output'] : true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats command aliases to show them in the command description.
|
||||
*/
|
||||
private function getCommandAliasesText(Command $command): string
|
||||
{
|
||||
$text = '';
|
||||
$aliases = $command->getAliases();
|
||||
|
||||
if ($aliases) {
|
||||
$text = '['.implode('|', $aliases).'] ';
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input option/argument default value.
|
||||
*/
|
||||
private function formatDefaultValue(mixed $default): string
|
||||
{
|
||||
if (\INF === $default) {
|
||||
return 'INF';
|
||||
}
|
||||
|
||||
if (\is_string($default)) {
|
||||
$default = OutputFormatter::escape($default);
|
||||
} elseif (\is_array($default)) {
|
||||
foreach ($default as $key => $value) {
|
||||
if (\is_string($value)) {
|
||||
$default[$key] = OutputFormatter::escape($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Command|string> $commands
|
||||
*/
|
||||
private function getColumnWidth(array $commands): int
|
||||
{
|
||||
$widths = [];
|
||||
|
||||
foreach ($commands as $command) {
|
||||
if ($command instanceof Command) {
|
||||
$widths[] = Helper::width($command->getName());
|
||||
foreach ($command->getAliases() as $alias) {
|
||||
$widths[] = Helper::width($alias);
|
||||
}
|
||||
} else {
|
||||
$widths[] = Helper::width($command);
|
||||
}
|
||||
}
|
||||
|
||||
return $widths ? max($widths) + 2 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputOption[] $options
|
||||
*/
|
||||
private function calculateTotalWidthForOptions(array $options): int
|
||||
{
|
||||
$totalWidth = 0;
|
||||
foreach ($options as $option) {
|
||||
// "-" + shortcut + ", --" + name
|
||||
$nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName());
|
||||
if ($option->isNegatable()) {
|
||||
$nameLength += 6 + Helper::width($option->getName()); // |--no- + name
|
||||
} elseif ($option->acceptValue()) {
|
||||
$valueLength = 1 + Helper::width($option->getName()); // = + value
|
||||
$valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
|
||||
|
||||
$nameLength += $valueLength;
|
||||
}
|
||||
$totalWidth = max($totalWidth, $nameLength);
|
||||
}
|
||||
|
||||
return $totalWidth;
|
||||
}
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Descriptor;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* XML descriptor.
|
||||
*
|
||||
* @author Jean-François Simon <contact@jfsimon.fr>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class XmlDescriptor extends Descriptor
|
||||
{
|
||||
public function getInputDefinitionDocument(InputDefinition $definition): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->appendChild($definitionXML = $dom->createElement('definition'));
|
||||
|
||||
$definitionXML->appendChild($argumentsXML = $dom->createElement('arguments'));
|
||||
foreach ($definition->getArguments() as $argument) {
|
||||
$this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument));
|
||||
}
|
||||
|
||||
$definitionXML->appendChild($optionsXML = $dom->createElement('options'));
|
||||
foreach ($definition->getOptions() as $option) {
|
||||
$this->appendDocument($optionsXML, $this->getInputOptionDocument($option));
|
||||
}
|
||||
|
||||
return $dom;
|
||||
}
|
||||
|
||||
public function getCommandDocument(Command $command, bool $short = false): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->appendChild($commandXML = $dom->createElement('command'));
|
||||
|
||||
$commandXML->setAttribute('id', $command->getName());
|
||||
$commandXML->setAttribute('name', $command->getName());
|
||||
$commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0);
|
||||
|
||||
$commandXML->appendChild($usagesXML = $dom->createElement('usages'));
|
||||
|
||||
$commandXML->appendChild($descriptionXML = $dom->createElement('description'));
|
||||
$descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())));
|
||||
|
||||
if ($short) {
|
||||
foreach ($command->getAliases() as $usage) {
|
||||
$usagesXML->appendChild($dom->createElement('usage', $usage));
|
||||
}
|
||||
} else {
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
|
||||
$usagesXML->appendChild($dom->createElement('usage', $usage));
|
||||
}
|
||||
|
||||
$commandXML->appendChild($helpXML = $dom->createElement('help'));
|
||||
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
|
||||
|
||||
$definitionXML = $this->getInputDefinitionDocument($command->getDefinition());
|
||||
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
|
||||
}
|
||||
|
||||
return $dom;
|
||||
}
|
||||
|
||||
public function getApplicationDocument(Application $application, ?string $namespace = null, bool $short = false): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->appendChild($rootXml = $dom->createElement('symfony'));
|
||||
|
||||
if ('UNKNOWN' !== $application->getName()) {
|
||||
$rootXml->setAttribute('name', $application->getName());
|
||||
if ('UNKNOWN' !== $application->getVersion()) {
|
||||
$rootXml->setAttribute('version', $application->getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
$rootXml->appendChild($commandsXML = $dom->createElement('commands'));
|
||||
|
||||
$description = new ApplicationDescription($application, $namespace, true);
|
||||
|
||||
if ($namespace) {
|
||||
$commandsXML->setAttribute('namespace', $namespace);
|
||||
}
|
||||
|
||||
foreach ($description->getCommands() as $command) {
|
||||
$this->appendDocument($commandsXML, $this->getCommandDocument($command, $short));
|
||||
}
|
||||
|
||||
if (!$namespace) {
|
||||
$rootXml->appendChild($namespacesXML = $dom->createElement('namespaces'));
|
||||
|
||||
foreach ($description->getNamespaces() as $namespaceDescription) {
|
||||
$namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace'));
|
||||
$namespaceArrayXML->setAttribute('id', $namespaceDescription['id']);
|
||||
|
||||
foreach ($namespaceDescription['commands'] as $name) {
|
||||
$namespaceArrayXML->appendChild($commandXML = $dom->createElement('command'));
|
||||
$commandXML->appendChild($dom->createTextNode($name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dom;
|
||||
}
|
||||
|
||||
protected function describeInputArgument(InputArgument $argument, array $options = []): void
|
||||
{
|
||||
$this->writeDocument($this->getInputArgumentDocument($argument));
|
||||
}
|
||||
|
||||
protected function describeInputOption(InputOption $option, array $options = []): void
|
||||
{
|
||||
$this->writeDocument($this->getInputOptionDocument($option));
|
||||
}
|
||||
|
||||
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
|
||||
{
|
||||
$this->writeDocument($this->getInputDefinitionDocument($definition));
|
||||
}
|
||||
|
||||
protected function describeCommand(Command $command, array $options = []): void
|
||||
{
|
||||
$this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
|
||||
}
|
||||
|
||||
protected function describeApplication(Application $application, array $options = []): void
|
||||
{
|
||||
$this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends document children to parent node.
|
||||
*/
|
||||
private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent): void
|
||||
{
|
||||
foreach ($importedParent->childNodes as $childNode) {
|
||||
$parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes DOM document.
|
||||
*/
|
||||
private function writeDocument(\DOMDocument $dom): void
|
||||
{
|
||||
$dom->formatOutput = true;
|
||||
$this->write($dom->saveXML());
|
||||
}
|
||||
|
||||
private function getInputArgumentDocument(InputArgument $argument): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
$dom->appendChild($objectXML = $dom->createElement('argument'));
|
||||
$objectXML->setAttribute('name', $argument->getName());
|
||||
$objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0);
|
||||
$objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0);
|
||||
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
|
||||
$descriptionXML->appendChild($dom->createTextNode($argument->getDescription()));
|
||||
|
||||
$objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
|
||||
$defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : []));
|
||||
foreach ($defaults as $default) {
|
||||
$defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
|
||||
$defaultXML->appendChild($dom->createTextNode($default));
|
||||
}
|
||||
|
||||
return $dom;
|
||||
}
|
||||
|
||||
private function getInputOptionDocument(InputOption $option): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
$dom->appendChild($objectXML = $dom->createElement('option'));
|
||||
$objectXML->setAttribute('name', '--'.$option->getName());
|
||||
$pos = strpos($option->getShortcut() ?? '', '|');
|
||||
if (false !== $pos) {
|
||||
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
|
||||
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
|
||||
} else {
|
||||
$objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : '');
|
||||
}
|
||||
$objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0);
|
||||
$objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0);
|
||||
$objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0);
|
||||
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
|
||||
$descriptionXML->appendChild($dom->createTextNode($option->getDescription()));
|
||||
|
||||
if ($option->acceptValue()) {
|
||||
$defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : []));
|
||||
$objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
|
||||
|
||||
foreach ($defaults as $default) {
|
||||
$defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
|
||||
$defaultXML->appendChild($dom->createTextNode($default));
|
||||
}
|
||||
}
|
||||
|
||||
if ($option->isNegatable()) {
|
||||
$dom->appendChild($objectXML = $dom->createElement('option'));
|
||||
$objectXML->setAttribute('name', '--no-'.$option->getName());
|
||||
$objectXML->setAttribute('shortcut', '');
|
||||
$objectXML->setAttribute('accept_value', 0);
|
||||
$objectXML->setAttribute('is_value_required', 0);
|
||||
$objectXML->setAttribute('is_multiple', 0);
|
||||
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
|
||||
$descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option'));
|
||||
}
|
||||
|
||||
return $dom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Event;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class ConsoleAlarmEvent extends ConsoleEvent
|
||||
{
|
||||
public function __construct(
|
||||
Command $command,
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
private int|false $exitCode = 0,
|
||||
) {
|
||||
parent::__construct($command, $input, $output);
|
||||
}
|
||||
|
||||
public function setExitCode(int $exitCode): void
|
||||
{
|
||||
if ($exitCode < 0 || $exitCode > 255) {
|
||||
throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
|
||||
}
|
||||
|
||||
$this->exitCode = $exitCode;
|
||||
}
|
||||
|
||||
public function abortExit(): void
|
||||
{
|
||||
$this->exitCode = false;
|
||||
}
|
||||
|
||||
public function getExitCode(): int|false
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Event;
|
||||
|
||||
/**
|
||||
* Allows to do things before the command is executed, like skipping the command or executing code before the command is
|
||||
* going to be executed.
|
||||
*
|
||||
* Changing the input arguments will have no effect.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class ConsoleCommandEvent extends ConsoleEvent
|
||||
{
|
||||
/**
|
||||
* The return code for skipped commands, this will also be passed into the terminate event.
|
||||
*/
|
||||
public const RETURN_CODE_DISABLED = 113;
|
||||
|
||||
/**
|
||||
* Indicates if the command should be run or skipped.
|
||||
*/
|
||||
private bool $commandShouldRun = true;
|
||||
|
||||
/**
|
||||
* Disables the command, so it won't be run.
|
||||
*/
|
||||
public function disableCommand(): bool
|
||||
{
|
||||
return $this->commandShouldRun = false;
|
||||
}
|
||||
|
||||
public function enableCommand(): bool
|
||||
{
|
||||
return $this->commandShouldRun = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the command is runnable, false otherwise.
|
||||
*/
|
||||
public function commandShouldRun(): bool
|
||||
{
|
||||
return $this->commandShouldRun;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Event;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Allows to handle throwables thrown while running a command.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
final class ConsoleErrorEvent extends ConsoleEvent
|
||||
{
|
||||
private int $exitCode;
|
||||
|
||||
public function __construct(
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
private \Throwable $error,
|
||||
?Command $command = null,
|
||||
) {
|
||||
parent::__construct($command, $input, $output);
|
||||
}
|
||||
|
||||
public function getError(): \Throwable
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function setError(\Throwable $error): void
|
||||
{
|
||||
$this->error = $error;
|
||||
}
|
||||
|
||||
public function setExitCode(int $exitCode): void
|
||||
{
|
||||
$this->exitCode = $exitCode;
|
||||
|
||||
$r = new \ReflectionProperty($this->error, 'code');
|
||||
$r->setValue($this->error, $this->exitCode);
|
||||
}
|
||||
|
||||
public function getExitCode(): int
|
||||
{
|
||||
return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Event;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Allows to inspect input and output of a command.
|
||||
*
|
||||
* @author Francesco Levorato <git@flevour.net>
|
||||
*/
|
||||
class ConsoleEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
protected ?Command $command,
|
||||
private InputInterface $input,
|
||||
private OutputInterface $output,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the command that is executed.
|
||||
*/
|
||||
public function getCommand(): ?Command
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the input instance.
|
||||
*/
|
||||
public function getInput(): InputInterface
|
||||
{
|
||||
return $this->input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the output instance.
|
||||
*/
|
||||
public function getOutput(): OutputInterface
|
||||
{
|
||||
return $this->output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Event;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author marie <marie@users.noreply.github.com>
|
||||
*/
|
||||
final class ConsoleSignalEvent extends ConsoleEvent
|
||||
{
|
||||
public function __construct(
|
||||
Command $command,
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
private int $handlingSignal,
|
||||
private int|false $exitCode = 0,
|
||||
) {
|
||||
parent::__construct($command, $input, $output);
|
||||
}
|
||||
|
||||
public function getHandlingSignal(): int
|
||||
{
|
||||
return $this->handlingSignal;
|
||||
}
|
||||
|
||||
public function setExitCode(int $exitCode): void
|
||||
{
|
||||
if ($exitCode < 0 || $exitCode > 255) {
|
||||
throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
|
||||
}
|
||||
|
||||
$this->exitCode = $exitCode;
|
||||
}
|
||||
|
||||
public function abortExit(): void
|
||||
{
|
||||
$this->exitCode = false;
|
||||
}
|
||||
|
||||
public function getExitCode(): int|false
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Event;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Allows to manipulate the exit code of a command after its execution.
|
||||
*
|
||||
* @author Francesco Levorato <git@flevour.net>
|
||||
* @author Jules Pietri <jules@heahprod.com>
|
||||
*/
|
||||
final class ConsoleTerminateEvent extends ConsoleEvent
|
||||
{
|
||||
public function __construct(
|
||||
Command $command,
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
private int $exitCode,
|
||||
private readonly ?int $interruptingSignal = null,
|
||||
) {
|
||||
parent::__construct($command, $input, $output);
|
||||
}
|
||||
|
||||
public function setExitCode(int $exitCode): void
|
||||
{
|
||||
$this->exitCode = $exitCode;
|
||||
}
|
||||
|
||||
public function getExitCode(): int
|
||||
{
|
||||
return $this->exitCode;
|
||||
}
|
||||
|
||||
public function getInterruptingSignal(): ?int
|
||||
{
|
||||
return $this->interruptingSignal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Event dispatched when constraint validation is needed for a question.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
class QuestionAnsweredEvent extends Event
|
||||
{
|
||||
private array $violations = [];
|
||||
|
||||
public function __construct(
|
||||
public readonly mixed $value,
|
||||
public readonly array $constraints,
|
||||
) {
|
||||
}
|
||||
|
||||
public function addViolation(string $message): void
|
||||
{
|
||||
$this->violations[] = $message;
|
||||
}
|
||||
|
||||
public function getViolations(): array
|
||||
{
|
||||
return $this->violations;
|
||||
}
|
||||
|
||||
public function hasViolations(): bool
|
||||
{
|
||||
return (bool) $this->violations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\EventListener;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\ConsoleEvents;
|
||||
use Symfony\Component\Console\Event\ConsoleErrorEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* @author James Halsall <james.t.halsall@googlemail.com>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
class ErrorListener implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ?LoggerInterface $logger = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onConsoleError(ConsoleErrorEvent $event): void
|
||||
{
|
||||
if (null === $this->logger) {
|
||||
return;
|
||||
}
|
||||
|
||||
$error = $event->getError();
|
||||
|
||||
if (!$inputString = self::getInputString($event)) {
|
||||
$this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
|
||||
}
|
||||
|
||||
public function onConsoleTerminate(ConsoleTerminateEvent $event): void
|
||||
{
|
||||
if (null === $this->logger) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exitCode = $event->getExitCode();
|
||||
|
||||
if (0 === $exitCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$inputString = self::getInputString($event)) {
|
||||
$this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ConsoleEvents::ERROR => ['onConsoleError', -128],
|
||||
ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128],
|
||||
];
|
||||
}
|
||||
|
||||
private static function getInputString(ConsoleEvent $event): string
|
||||
{
|
||||
$commandName = $event->getCommand()?->getName();
|
||||
$inputString = (string) $event->getInput();
|
||||
|
||||
if ($commandName) {
|
||||
return str_replace(["'$commandName'", "\"$commandName\""], $commandName, $inputString);
|
||||
}
|
||||
|
||||
return $inputString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\EventListener;
|
||||
|
||||
use Symfony\Component\Console\ConsoleEvents;
|
||||
use Symfony\Component\Console\Event\QuestionAnsweredEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* Validates Question answers (user input) using the Validator component.
|
||||
*
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class ValidateQuestionInputListener implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ValidatorInterface $validator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onQuestionAnswered(QuestionAnsweredEvent $event): void
|
||||
{
|
||||
$violations = $this->validator->validate($event->value, $event->constraints);
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$event->addViolation($violation->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [ConsoleEvents::QUESTION_ANSWERED => 'onQuestionAnswered'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* Represents an incorrect command name typed in the console.
|
||||
*
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @param string $message Exception message to throw
|
||||
* @param string[] $alternatives List of similar defined names
|
||||
* @param int $code Exception code
|
||||
* @param \Throwable|null $previous Previous exception used for the exception chaining
|
||||
*/
|
||||
public function __construct(
|
||||
string $message,
|
||||
private array $alternatives = [],
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAlternatives(): array
|
||||
{
|
||||
return $this->alternatives;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* ExceptionInterface.
|
||||
*
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
|
||||
/**
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
final class InputValidationFailedException extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
private readonly ConstraintViolationListInterface $violations,
|
||||
) {
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
public function getViolations(): ConstraintViolationListInterface
|
||||
{
|
||||
return $this->violations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function fromEnumValue(string $name, string $value, array|\Closure $suggestedValues): self
|
||||
{
|
||||
$error = \sprintf('The value "%s" is not valid for the "%s" argument.', $value, $name);
|
||||
|
||||
if (\is_array($suggestedValues)) {
|
||||
$error .= \sprintf(' Supported values are "%s".', implode('", "', $suggestedValues));
|
||||
}
|
||||
|
||||
return new self($error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
*/
|
||||
class InvalidFileException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* Represents an incorrect option name or value typed in the console.
|
||||
*
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function fromEnumValue(string $name, string $value, array|\Closure $suggestedValues): self
|
||||
{
|
||||
$error = \sprintf('The value "%s" is not valid for the "%s" option.', $value, $name);
|
||||
|
||||
if (\is_array($suggestedValues)) {
|
||||
$error .= \sprintf(' Supported values are "%s".', implode('", "', $suggestedValues));
|
||||
}
|
||||
|
||||
return new self($error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class LogicException extends \LogicException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* Represents failure to read input from stdin.
|
||||
*
|
||||
* @author Gabriel Ostrolucký <gabriel.ostrolucky@gmail.com>
|
||||
*/
|
||||
class MissingInputException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Console\Exception;
|
||||
|
||||
/**
|
||||
* Represents an incorrect namespace typed in the console.
|
||||
*
|
||||
* @author Pierre du Plessis <pdples@gmail.com>
|
||||
*/
|
||||
class NamespaceNotFoundException extends CommandNotFoundException
|
||||
{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user