diff --git a/composer.json b/composer.json index 6f738377..1d091a74 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,10 @@ "psr/container": "^1.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0|^2.0|^3.0", + "symfony/config": "^5.0|^6.0", "symfony/console": "^5.0|^6.0", + "symfony/dependency-injection": "^5.0|^6.0", + "symfony/http-kernel": "^5.0|^6.0", "symfony/framework-bundle": "^5.0|^6.0", "symfony/messenger": "^5.0|^6.0", "symfony/serializer": "^5.0|^6.0", @@ -45,6 +48,7 @@ "yokai/batch-doctrine-dbal": "self.version", "yokai/batch-doctrine-orm": "self.version", "yokai/batch-doctrine-persistence": "self.version", + "yokai/batch-flysystem": "self.version", "yokai/batch-symfony-console": "self.version", "yokai/batch-symfony-framework": "self.version", "yokai/batch-symfony-messenger": "self.version", diff --git a/src/batch-box-spout/src/Reader/HeaderStrategy.php b/src/batch-box-spout/src/Reader/HeaderStrategy.php index b210a23f..c7f3279e 100644 --- a/src/batch-box-spout/src/Reader/HeaderStrategy.php +++ b/src/batch-box-spout/src/Reader/HeaderStrategy.php @@ -75,6 +75,8 @@ public function setHeaders(array $headers): bool } /** + * Build the associative item, a combination of headers and values. + * * @throws InvalidRowSizeException * * @phpstan-param array $row diff --git a/src/batch-box-spout/src/Reader/Options/SheetFilter.php b/src/batch-box-spout/src/Reader/Options/SheetFilter.php index 7648470c..bb837f3a 100644 --- a/src/batch-box-spout/src/Reader/Options/SheetFilter.php +++ b/src/batch-box-spout/src/Reader/Options/SheetFilter.php @@ -55,6 +55,8 @@ public static function nameIs(string $name, string ...$names): self } /** + * Iterate over valid sheets for the provided filter. + * * @return Generator&SheetInterface[] * @phpstan-return Generator * @internal diff --git a/src/batch-box-spout/src/Writer/WriteToSheetItem.php b/src/batch-box-spout/src/Writer/WriteToSheetItem.php index a8a62aec..5c312dac 100644 --- a/src/batch-box-spout/src/Writer/WriteToSheetItem.php +++ b/src/batch-box-spout/src/Writer/WriteToSheetItem.php @@ -21,6 +21,8 @@ private function __construct( } /** + * Static constructor from array data. + * * @param array $item */ public static function array(string $sheet, array $item, Style $style = null): self @@ -28,6 +30,9 @@ public static function array(string $sheet, array $item, Style $style = null): s return new self($sheet, WriterEntityFactory::createRowFromArray($item, $style)); } + /** + * Static constructor from {@see Row} object. + */ public static function row(string $sheet, Row $item): self { return new self($sheet, $item); diff --git a/src/batch-doctrine-dbal/composer.json b/src/batch-doctrine-dbal/composer.json index 76622b9a..8e3fc3b5 100644 --- a/src/batch-doctrine-dbal/composer.json +++ b/src/batch-doctrine-dbal/composer.json @@ -14,6 +14,7 @@ "php": "^8.0", "ext-json": "*", "doctrine/dbal": "^2.11", + "doctrine/persistence": "^2.0", "yokai/batch": "^0.4.0" }, "autoload": { diff --git a/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php b/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php index 2db86c89..2d4c7b3e 100644 --- a/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php +++ b/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php @@ -17,9 +17,14 @@ use Yokai\Batch\Exception\JobExecutionNotFoundException; use Yokai\Batch\Exception\RuntimeException; use Yokai\Batch\JobExecution; +use Yokai\Batch\Storage\JobExecutionStorageInterface; use Yokai\Batch\Storage\Query; use Yokai\Batch\Storage\QueryableJobExecutionStorageInterface; +/** + * This {@see JobExecutionStorageInterface} will store + * {@see JobExecution} in an SQL database using doctrine/dbal. + */ final class DoctrineDBALJobExecutionStorage implements QueryableJobExecutionStorageInterface { private const DEFAULT_OPTIONS = [ @@ -46,6 +51,9 @@ public function __construct(ConnectionRegistry $doctrine, array $options) $this->connection = $connection; } + /** + * Create required table for this storage. + */ public function createSchema(): void { $assetFilter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); diff --git a/src/batch-doctrine-dbal/src/JobExecutionRowNormalizer.php b/src/batch-doctrine-dbal/src/JobExecutionRowNormalizer.php index 1411c991..23d437fa 100644 --- a/src/batch-doctrine-dbal/src/JobExecutionRowNormalizer.php +++ b/src/batch-doctrine-dbal/src/JobExecutionRowNormalizer.php @@ -27,6 +27,8 @@ public function __construct( } /** + * Convert a {@see JobExecution} object to a row data array. + * * @phpstan-return array */ public function toRow(JobExecution $jobExecution): array @@ -47,6 +49,8 @@ public function toRow(JobExecution $jobExecution): array } /** + * Convert a row data array to a {@see JobExecution} object. + * * @phpstan-param array $data */ public function fromRow(array $data, JobExecution $parent = null): JobExecution @@ -97,7 +101,7 @@ public function fromRow(array $data, JobExecution $parent = null): JobExecution /** * @phpstan-return array */ - public function toChildRow(JobExecution $jobExecution): array + private function toChildRow(JobExecution $jobExecution): array { return [ 'job_name' => $jobExecution->getJobName(), diff --git a/src/batch-doctrine-orm/composer.json b/src/batch-doctrine-orm/composer.json index 2f5866da..472b1e8f 100644 --- a/src/batch-doctrine-orm/composer.json +++ b/src/batch-doctrine-orm/composer.json @@ -13,6 +13,7 @@ "require": { "php": "^8.0", "doctrine/orm": "^2.8", + "doctrine/persistence": "^2.0", "yokai/batch": "^0.4.0" }, "autoload": { diff --git a/src/batch-symfony-console/src/CommandRunner.php b/src/batch-symfony-console/src/CommandRunner.php index 0aacf511..029a0045 100644 --- a/src/batch-symfony-console/src/CommandRunner.php +++ b/src/batch-symfony-console/src/CommandRunner.php @@ -7,6 +7,9 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Process\PhpExecutableFinder; +/** + * Utility class that knows how to run command asynchronously. + */ class CommandRunner { private string $consolePath; @@ -25,6 +28,8 @@ public function __construct( } /** + * Run a command asynchronously. + * * @phpstan-param array $arguments */ public function runAsync(string $commandName, string $logFilename, array $arguments = []): void diff --git a/src/batch-symfony-console/src/RunCommandJobLauncher.php b/src/batch-symfony-console/src/RunCommandJobLauncher.php index 767815c7..7a1d6a37 100644 --- a/src/batch-symfony-console/src/RunCommandJobLauncher.php +++ b/src/batch-symfony-console/src/RunCommandJobLauncher.php @@ -13,7 +13,7 @@ /** * This {@see JobLauncherInterface} will execute job via an asynchronous symfony command. * - * Example, if you call {@see RunCommandJobLauncher::launch('import', ['foo'=>'bar'])}, + * Example, if you call RunCommandJobLauncher::launch('import', ['foo'=>'bar']), * this command will run (with absolute pathes) : * * php bin/console yokai:batch:run import '{"foo":"bar"}' >> var/log/batch_execute.log 2>&1 & diff --git a/src/batch-symfony-console/src/RunJobCommand.php b/src/batch-symfony-console/src/RunJobCommand.php index 986d5503..22fec183 100644 --- a/src/batch-symfony-console/src/RunJobCommand.php +++ b/src/batch-symfony-console/src/RunJobCommand.php @@ -13,8 +13,12 @@ use Yokai\Batch\Exception\UnexpectedValueException; use Yokai\Batch\Job\JobExecutionAccessor; use Yokai\Batch\Job\JobExecutor; +use Yokai\Batch\Job\JobInterface; use Yokai\Batch\JobExecution; +/** + * Execute any {@see JobInterface} within your CLI. + */ final class RunJobCommand extends Command { protected static $defaultName = 'yokai:batch:run'; diff --git a/src/batch-symfony-framework/composer.json b/src/batch-symfony-framework/composer.json index 34f6eded..6fcba80a 100644 --- a/src/batch-symfony-framework/composer.json +++ b/src/batch-symfony-framework/composer.json @@ -13,6 +13,9 @@ "require": { "php": "^8.0", "composer-runtime-api": "^2.0", + "symfony/config": "^5.0|^6.0", + "symfony/dependency-injection": "^5.0|^6.0", + "symfony/http-kernel": "^5.0|^6.0", "symfony/framework-bundle": "^5.0|^6.0", "yokai/batch": "^0.4.0" }, diff --git a/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/RegisterJobsCompilerPass.php b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/RegisterJobsCompilerPass.php index b66b3e66..ea07453e 100644 --- a/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/RegisterJobsCompilerPass.php +++ b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/RegisterJobsCompilerPass.php @@ -10,7 +10,12 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Yokai\Batch\Bridge\Symfony\Framework\JobWithStaticNameInterface; +use Yokai\Batch\Job\JobInterface; +use Yokai\Batch\Registry\JobRegistry; +/** + * Find tagged {@see JobInterface} and register these in {@see JobRegistry}. + */ final class RegisterJobsCompilerPass implements CompilerPassInterface { /** diff --git a/src/batch-symfony-framework/src/DependencyInjection/Configuration.php b/src/batch-symfony-framework/src/DependencyInjection/Configuration.php index 0424f159..0441bc8b 100644 --- a/src/batch-symfony-framework/src/DependencyInjection/Configuration.php +++ b/src/batch-symfony-framework/src/DependencyInjection/Configuration.php @@ -8,6 +8,9 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +/** + * Configuration for yokai/batch Symfony Bundle. + */ final class Configuration implements ConfigurationInterface { /** diff --git a/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php b/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php index 2f67857a..5df34c9c 100644 --- a/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php +++ b/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php @@ -20,6 +20,9 @@ use Yokai\Batch\Storage\ListableJobExecutionStorageInterface; use Yokai\Batch\Storage\QueryableJobExecutionStorageInterface; +/** + * Dependency injection extension for yokai/batch Symfony Bundle. + */ final class YokaiBatchExtension extends Extension { /** diff --git a/src/batch-symfony-framework/src/JobWithStaticNameInterface.php b/src/batch-symfony-framework/src/JobWithStaticNameInterface.php index b4685d37..2fafadb3 100644 --- a/src/batch-symfony-framework/src/JobWithStaticNameInterface.php +++ b/src/batch-symfony-framework/src/JobWithStaticNameInterface.php @@ -4,6 +4,8 @@ namespace Yokai\Batch\Bridge\Symfony\Framework; +use Yokai\Batch\Registry\JobRegistry; + /** * A job that implement this interface can define the associated job name via a static method. * This is very useful if you are registering jobs using PSR services registering. @@ -12,5 +14,8 @@ */ interface JobWithStaticNameInterface { + /** + * The job name as it will be registered in the {@see JobRegistry}. + */ public static function getJobName(): string; } diff --git a/src/batch-symfony-framework/src/YokaiBatchBundle.php b/src/batch-symfony-framework/src/YokaiBatchBundle.php index 7156378f..09a7685d 100644 --- a/src/batch-symfony-framework/src/YokaiBatchBundle.php +++ b/src/batch-symfony-framework/src/YokaiBatchBundle.php @@ -8,6 +8,9 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\RegisterJobsCompilerPass; +/** + * yokai/batch Symfony Bundle. + */ final class YokaiBatchBundle extends Bundle { public function build(ContainerBuilder $container): void diff --git a/src/batch/src/Event/JobEvent.php b/src/batch/src/Event/JobEvent.php index 0e4f7d6d..1797e6a0 100644 --- a/src/batch/src/Event/JobEvent.php +++ b/src/batch/src/Event/JobEvent.php @@ -6,6 +6,9 @@ use Yokai\Batch\JobExecution; +/** + * Base class for all job execution related events. + */ class JobEvent { public function __construct( diff --git a/src/batch/src/Event/PostExecuteEvent.php b/src/batch/src/Event/PostExecuteEvent.php index 48f45cba..02586e6e 100644 --- a/src/batch/src/Event/PostExecuteEvent.php +++ b/src/batch/src/Event/PostExecuteEvent.php @@ -4,6 +4,10 @@ namespace Yokai\Batch\Event; +/** + * This event is triggered by {@see JobExecutor} + * whenever a job execution fails or succeed. + */ final class PostExecuteEvent extends JobEvent { } diff --git a/src/batch/src/Event/PreExecuteEvent.php b/src/batch/src/Event/PreExecuteEvent.php index 23457f8c..5274a1ee 100644 --- a/src/batch/src/Event/PreExecuteEvent.php +++ b/src/batch/src/Event/PreExecuteEvent.php @@ -4,6 +4,12 @@ namespace Yokai\Batch\Event; +use Yokai\Batch\Job\JobExecutor; + +/** + * This event is triggered by {@see JobExecutor} + * whenever a job execution starts. + */ final class PreExecuteEvent extends JobEvent { } diff --git a/src/batch/src/Exception/ExceptionInterface.php b/src/batch/src/Exception/ExceptionInterface.php index b81414a1..2a7c6eee 100644 --- a/src/batch/src/Exception/ExceptionInterface.php +++ b/src/batch/src/Exception/ExceptionInterface.php @@ -6,6 +6,9 @@ use Throwable; +/** + * Interface for all exceptions triggered by this library. + */ interface ExceptionInterface extends Throwable { } diff --git a/src/batch/src/Exception/UndefinedJobException.php b/src/batch/src/Exception/UndefinedJobException.php index b3ae2213..ba985c38 100644 --- a/src/batch/src/Exception/UndefinedJobException.php +++ b/src/batch/src/Exception/UndefinedJobException.php @@ -4,10 +4,12 @@ namespace Yokai\Batch\Exception; +use Throwable; + class UndefinedJobException extends InvalidArgumentException { - public function __construct(string $name) + public function __construct(string $name, Throwable $previous = null) { - parent::__construct(sprintf('Job "%s" is undefined', $name)); + parent::__construct(sprintf('Job "%s" is undefined', $name), $previous); } } diff --git a/src/batch/src/Factory/JobExecutionFactory.php b/src/batch/src/Factory/JobExecutionFactory.php index e65e1132..a63b1041 100644 --- a/src/batch/src/Factory/JobExecutionFactory.php +++ b/src/batch/src/Factory/JobExecutionFactory.php @@ -7,6 +7,9 @@ use Yokai\Batch\JobExecution; use Yokai\Batch\JobParameters; +/** + * Create a {@see JobExecution} from scalar members. + */ final class JobExecutionFactory { public function __construct( @@ -15,6 +18,8 @@ public function __construct( } /** + * Create a {@see JobExecution}. + * * @phpstan-param array $configuration */ public function create(string $name, array $configuration = []): JobExecution diff --git a/src/batch/src/Factory/JobExecutionIdGeneratorInterface.php b/src/batch/src/Factory/JobExecutionIdGeneratorInterface.php index 6e32459b..e2c06157 100644 --- a/src/batch/src/Factory/JobExecutionIdGeneratorInterface.php +++ b/src/batch/src/Factory/JobExecutionIdGeneratorInterface.php @@ -11,5 +11,8 @@ */ interface JobExecutionIdGeneratorInterface { + /** + * Generate and return a new id for the {@see JobExecution}. + */ public function generate(): string; } diff --git a/src/batch/src/Factory/UniqidJobExecutionIdGenerator.php b/src/batch/src/Factory/UniqidJobExecutionIdGenerator.php index e72fedaf..bb8e8c52 100644 --- a/src/batch/src/Factory/UniqidJobExecutionIdGenerator.php +++ b/src/batch/src/Factory/UniqidJobExecutionIdGenerator.php @@ -4,6 +4,10 @@ namespace Yokai\Batch\Factory; +/** + * This {@see JobExecutionIdGeneratorInterface} will use + * php {@see uniqid} function to generate job ids. + */ final class UniqidJobExecutionIdGenerator implements JobExecutionIdGeneratorInterface { /** diff --git a/src/batch/src/Failure.php b/src/batch/src/Failure.php index 88e7e23c..063ecfb7 100644 --- a/src/batch/src/Failure.php +++ b/src/batch/src/Failure.php @@ -38,6 +38,8 @@ public function __construct( } /** + * Static constructor from an exception. + * * @phpstan-param array $parameters */ public static function fromException(Throwable $exception, array $parameters = []): self diff --git a/src/batch/src/Job/Item/Processor/CallbackProcessor.php b/src/batch/src/Job/Item/Processor/CallbackProcessor.php index a1498906..6830d770 100644 --- a/src/batch/src/Job/Item/Processor/CallbackProcessor.php +++ b/src/batch/src/Job/Item/Processor/CallbackProcessor.php @@ -7,13 +7,15 @@ use Closure; use Yokai\Batch\Job\Item\ItemProcessorInterface; +/** + * This {@see ItemProcessorInterface} will transform every item + * with a closure provided at object's construction. + */ final class CallbackProcessor implements ItemProcessorInterface { - private Closure $callback; - - public function __construct(Closure $callback) - { - $this->callback = $callback; + public function __construct( + private Closure $callback, + ) { } public function process(mixed $item): mixed diff --git a/src/batch/src/Job/JobExecutionAccessor.php b/src/batch/src/Job/JobExecutionAccessor.php index 3fbbcc10..24d83c69 100644 --- a/src/batch/src/Job/JobExecutionAccessor.php +++ b/src/batch/src/Job/JobExecutionAccessor.php @@ -23,6 +23,8 @@ public function __construct( } /** + * Retrieve or create a {@see JobExecution}. + * * @param array $configuration */ public function get(string $name, array $configuration): JobExecution diff --git a/src/batch/src/Job/JobExecutionAwareInterface.php b/src/batch/src/Job/JobExecutionAwareInterface.php index 59fcfda3..fc1c6aa0 100644 --- a/src/batch/src/Job/JobExecutionAwareInterface.php +++ b/src/batch/src/Job/JobExecutionAwareInterface.php @@ -9,8 +9,13 @@ /** * A class implementing this interface will gain access * to current {@see JobExecution}. + * + * Default implementation from {@see JobExecutionAwareTrait} can be used. */ interface JobExecutionAwareInterface { + /** + * Set execution to the job component. + */ public function setJobExecution(JobExecution $jobExecution): void; } diff --git a/src/batch/src/Job/JobExecutionAwareTrait.php b/src/batch/src/Job/JobExecutionAwareTrait.php index ce17b568..870153dc 100644 --- a/src/batch/src/Job/JobExecutionAwareTrait.php +++ b/src/batch/src/Job/JobExecutionAwareTrait.php @@ -21,6 +21,9 @@ public function setJobExecution(JobExecution $jobExecution): void $this->jobExecution = $jobExecution; } + /** + * Get root execution of current job execution. + */ public function getRootExecution(): JobExecution { return $this->jobExecution->getRootExecution(); diff --git a/src/batch/src/Job/JobExecutor.php b/src/batch/src/Job/JobExecutor.php index 54269227..5c67e82a 100644 --- a/src/batch/src/Job/JobExecutor.php +++ b/src/batch/src/Job/JobExecutor.php @@ -33,6 +33,9 @@ public function __construct( ) { } + /** + * Execute job and respect rules around it. + */ public function execute(JobExecution $jobExecution): void { $logger = $jobExecution->getLogger(); diff --git a/src/batch/src/Job/JobParametersAwareInterface.php b/src/batch/src/Job/JobParametersAwareInterface.php index 85a58792..f04fb12b 100644 --- a/src/batch/src/Job/JobParametersAwareInterface.php +++ b/src/batch/src/Job/JobParametersAwareInterface.php @@ -13,8 +13,13 @@ * * Parameters can also be accessed by implementing {@see JobExecutionAwareInterface} * and calling {@see JobExecution::getParameters} on the provided execution. + * + * Default implementation from {@see JobParametersAwareTrait} can be used. */ interface JobParametersAwareInterface { + /** + * Set parameters to the job component. + */ public function setJobParameters(JobParameters $parameters): void; } diff --git a/src/batch/src/Job/JobWithChildJobs.php b/src/batch/src/Job/JobWithChildJobs.php index c0f2692c..6312d674 100644 --- a/src/batch/src/Job/JobWithChildJobs.php +++ b/src/batch/src/Job/JobWithChildJobs.php @@ -8,6 +8,10 @@ use Yokai\Batch\JobExecution; use Yokai\Batch\Storage\JobExecutionStorageInterface; +/** + * This {@see JobInterface} will execute by triggering child jobs. + * If a child job fails, following child jobs won't be executed. + */ class JobWithChildJobs implements JobInterface { public function __construct( diff --git a/src/batch/src/Job/Parameters/JobParameterAccessorInterface.php b/src/batch/src/Job/Parameters/JobParameterAccessorInterface.php index 1ea74833..c0f7f620 100644 --- a/src/batch/src/Job/Parameters/JobParameterAccessorInterface.php +++ b/src/batch/src/Job/Parameters/JobParameterAccessorInterface.php @@ -14,6 +14,8 @@ interface JobParameterAccessorInterface { /** + * Get the parameter value from job execution. + * * @param JobExecution $execution A job execution (for context) * * @return mixed The requested value diff --git a/src/batch/src/Job/SummaryAwareInterface.php b/src/batch/src/Job/SummaryAwareInterface.php index ebbc7ae3..7c8b8e5c 100644 --- a/src/batch/src/Job/SummaryAwareInterface.php +++ b/src/batch/src/Job/SummaryAwareInterface.php @@ -12,8 +12,13 @@ * * Summary can also be accessed by implementing {@see JobExecutionAwareInterface} * and calling {@see JobExecution::getSummary} on the provided execution. + * + * Default implementation from {@see SummaryAwareTrait} can be used. */ interface SummaryAwareInterface { + /** + * Set summary to the job component. + */ public function setSummary(Summary $summary): void; } diff --git a/src/batch/src/JobExecution.php b/src/batch/src/JobExecution.php index f9d76f03..3974f60b 100644 --- a/src/batch/src/JobExecution.php +++ b/src/batch/src/JobExecution.php @@ -176,6 +176,10 @@ public function getEndTime(): ?DateTimeInterface return $this->endTime; } + /** + * Build a {@see DateInterval} + * from {@see JobExecution::$startTime} to {@see JobExecution::$endTime}. + */ public function getDuration(): DateInterval { $now = new DateTime(); @@ -248,6 +252,9 @@ public function getChildExecutions(): array return array_values($this->childExecutions); } + /** + * Add a child execution. + */ public function addChildExecution(JobExecution $execution): void { $this->childExecutions[$execution->getJobName()] = $execution; @@ -267,6 +274,9 @@ public function getParameters(): JobParameters return $this->parameters; } + /** + * Get a parameter value. + */ public function getParameter(string $name): mixed { return $this->parameters->get($name); @@ -280,6 +290,9 @@ public function getFailures(): array return $this->failures; } + /** + * Add a failure to the execution. + */ public function addFailure(Failure $failure, bool $log = true): void { $this->failures[] = $failure; @@ -289,6 +302,8 @@ public function addFailure(Failure $failure, bool $log = true): void } /** + * Add a failure, build from exception, to the execution. + * * @phpstan-param array $parameters */ public function addFailureException(Throwable $exception, array $parameters = [], bool $log = true): void @@ -324,6 +339,9 @@ public function getWarnings(): array return $this->warnings; } + /** + * Add a warning to the execution. + */ public function addWarning(Warning $warning, bool $log = true): void { $this->warnings[] = $warning; diff --git a/src/batch/src/JobExecutionLogs.php b/src/batch/src/JobExecutionLogs.php index 2b23f7ec..5cb2639d 100644 --- a/src/batch/src/JobExecutionLogs.php +++ b/src/batch/src/JobExecutionLogs.php @@ -22,6 +22,9 @@ public function __toString(): string return $this->logs; } + /** + * Append message to logs. + */ public function log(string $message): void { $this->logs .= $message . PHP_EOL; diff --git a/src/batch/src/JobParameters.php b/src/batch/src/JobParameters.php index 673d4dbd..7e1bf85a 100644 --- a/src/batch/src/JobParameters.php +++ b/src/batch/src/JobParameters.php @@ -30,6 +30,8 @@ public function __construct( } /** + * Get all parameter values. + * * @phpstan-return array */ public function all(): array @@ -46,6 +48,8 @@ public function has(string $name): bool } /** + * Get a parameter value. + * * @throws UndefinedJobParameterException If parameter is not defined */ public function get(string $name): mixed diff --git a/src/batch/src/Registry/JobRegistry.php b/src/batch/src/Registry/JobRegistry.php index c914fdc1..19743190 100644 --- a/src/batch/src/Registry/JobRegistry.php +++ b/src/batch/src/Registry/JobRegistry.php @@ -4,10 +4,17 @@ namespace Yokai\Batch\Registry; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Yokai\Batch\Exception\UndefinedJobException; use Yokai\Batch\Job\JobInterface; +/** + * This class is a wrapper around a {@see ContainerInterface}, + * responsible for accessing jobs in a typed maner. + * It can be registered as a global registry, + * but it can be also created with a subset of jobs if required. + */ final class JobRegistry { public function __construct( @@ -16,6 +23,8 @@ public function __construct( } /** + * Static constructor with indexed array of jobs. + * * @param array $jobs */ public static function fromJobArray(array $jobs): JobRegistry @@ -24,17 +33,19 @@ public static function fromJobArray(array $jobs): JobRegistry } /** - * @throws UndefinedJobException + * Get a job from its name. + * + * @throws UndefinedJobException if the job is not defined */ public function get(string $name): JobInterface { - if (!$this->jobs->has($name)) { - throw new UndefinedJobException($name); + try { + /** @var JobInterface $job */ + $job = $this->jobs->get($name); + } catch (ContainerExceptionInterface $exception) { + throw new UndefinedJobException($name, $exception); } - /** @var JobInterface $job */ - $job = $this->jobs->get($name); - return $job; } } diff --git a/src/batch/src/Serializer/JsonJobExecutionSerializer.php b/src/batch/src/Serializer/JsonJobExecutionSerializer.php index 85d3b179..468a6025 100644 --- a/src/batch/src/Serializer/JsonJobExecutionSerializer.php +++ b/src/batch/src/Serializer/JsonJobExecutionSerializer.php @@ -15,9 +15,14 @@ use Yokai\Batch\JobExecution; use Yokai\Batch\JobExecutionLogs; use Yokai\Batch\JobParameters; +use Yokai\Batch\Storage\JobExecutionStorageInterface; use Yokai\Batch\Summary; use Yokai\Batch\Warning; +/** + * This {@see JobExecutionStorageInterface} will (un)serialise any {@see JobExecution} to/from json, + * using internal (de)normalisation and PHP {@see json_encode} and {@see json_decode} functions. + */ final class JsonJobExecutionSerializer implements JobExecutionSerializerInterface { /** diff --git a/src/batch/src/Storage/FilesystemJobExecutionStorage.php b/src/batch/src/Storage/FilesystemJobExecutionStorage.php index a2f5e148..b7804a0c 100644 --- a/src/batch/src/Storage/FilesystemJobExecutionStorage.php +++ b/src/batch/src/Storage/FilesystemJobExecutionStorage.php @@ -154,7 +154,7 @@ public function query(Query $query): iterable return array_slice($candidates, $query->offset(), $query->limit()); } - public function buildFilePath(string $jobName, string $executionId): string + private function buildFilePath(string $jobName, string $executionId): string { return implode(DIRECTORY_SEPARATOR, [$this->directory, $jobName, $executionId]) . '.' . $this->serializer->extension(); diff --git a/src/batch/src/Storage/ListableJobExecutionStorageInterface.php b/src/batch/src/Storage/ListableJobExecutionStorageInterface.php index 3ef7666a..6f8d0fae 100644 --- a/src/batch/src/Storage/ListableJobExecutionStorageInterface.php +++ b/src/batch/src/Storage/ListableJobExecutionStorageInterface.php @@ -12,6 +12,8 @@ interface ListableJobExecutionStorageInterface extends JobExecutionStorageInterface { /** + * List all job executions that are for the given job. + * * @return iterable|JobExecution[] */ public function list(string $jobName): iterable; diff --git a/src/batch/src/Storage/Query.php b/src/batch/src/Storage/Query.php index e1ec6434..2ac17b0a 100644 --- a/src/batch/src/Storage/Query.php +++ b/src/batch/src/Storage/Query.php @@ -24,7 +24,7 @@ public function __construct( /** * @var string[] */ - private array $jobNames, + private array $jobs, /** * @var string[] */ @@ -33,7 +33,7 @@ public function __construct( * @var int[] */ private array $statuses, - private ?string $sortBy, + private ?string $sort, private int $limit, private int $offset = 0, ) { @@ -44,7 +44,7 @@ public function __construct( */ public function jobs(): array { - return $this->jobNames; + return $this->jobs; } /** @@ -65,7 +65,7 @@ public function statuses(): array public function sort(): ?string { - return $this->sortBy; + return $this->sort; } public function limit(): int diff --git a/src/batch/src/Storage/QueryBuilder.php b/src/batch/src/Storage/QueryBuilder.php index 9e2c73d5..afa0ccf1 100644 --- a/src/batch/src/Storage/QueryBuilder.php +++ b/src/batch/src/Storage/QueryBuilder.php @@ -70,9 +70,9 @@ final class QueryBuilder private int $offset = 0; /** - * @param string[] $names + * Filter executions that are for one of the given job names. * - * @return $this + * @param string[] $names */ public function jobs(array $names): self { @@ -89,9 +89,9 @@ public function jobs(array $names): self } /** - * @param string[] $ids + * Filter executions that are one of given ids. * - * @return $this + * @param string[] $ids */ public function ids(array $ids): self { @@ -108,9 +108,9 @@ public function ids(array $ids): self } /** - * @param int[] $statuses + * Filter executions that are on given status. * - * @return $this + * @param int[] $statuses Any of {@see BatchStatus::*} */ public function statuses(array $statuses): self { @@ -127,7 +127,9 @@ public function statuses(array $statuses): self } /** - * @return $this + * Sort executions. + * + * @param string $by One of {@see QueryBuilder::SORT_BY_*} */ public function sort(string $by): self { @@ -141,7 +143,7 @@ public function sort(string $by): self } /** - * @return $this + * Limit query to a certain amount of executions. */ public function limit(int $limit, int $offset): self { @@ -158,6 +160,9 @@ public function limit(int $limit, int $offset): self return $this; } + /** + * Build query from criteria in this builder. + */ public function getQuery(): Query { return new Query($this->jobNames, $this->ids, $this->statuses, $this->sortBy, $this->limit, $this->offset); diff --git a/src/batch/src/Storage/QueryableJobExecutionStorageInterface.php b/src/batch/src/Storage/QueryableJobExecutionStorageInterface.php index 29aa2326..50f798f9 100644 --- a/src/batch/src/Storage/QueryableJobExecutionStorageInterface.php +++ b/src/batch/src/Storage/QueryableJobExecutionStorageInterface.php @@ -12,6 +12,8 @@ interface QueryableJobExecutionStorageInterface extends ListableJobExecutionStorageInterface { /** + * Execute query against stored job executions, and return the matching list. + * * @return iterable|JobExecution[] */ public function query(Query $query): iterable; diff --git a/src/batch/src/Summary.php b/src/batch/src/Summary.php index 514eb27a..eee26df0 100644 --- a/src/batch/src/Summary.php +++ b/src/batch/src/Summary.php @@ -31,16 +31,25 @@ public function __construct( ) { } + /** + * Set value. + */ public function set(string $key, mixed $info): void { $this->values[$key] = $info; } + /** + * Handle a numeric value by incrementing value of it. + */ public function increment(string $key, float|int $increment = 1): void { $this->values[$key] = ($this->values[$key] ?? 0) + $increment; } + /** + * Handle an array value by appending a new value to it. + */ public function append(string $key, mixed $value): void { $this->values[$key] ??= []; @@ -51,17 +60,25 @@ public function append(string $key, mixed $value): void $this->values[$key][] = $value; } + /** + * Get a value, or null if not set. + */ public function get(string $key): mixed { return $this->values[$key] ?? null; } + /** + * Whether a value was set. + */ public function has(string $key): bool { return array_key_exists($key, $this->values); } /** + * Get all values. + * * @phpstan-return array */ public function all(): array @@ -69,6 +86,9 @@ public function all(): array return $this->values; } + /** + * Clear all values. + */ public function clear(): void { $this->values = []; diff --git a/src/batch/src/Test/Factory/SequenceJobExecutionIdGenerator.php b/src/batch/src/Test/Factory/SequenceJobExecutionIdGenerator.php index 04dd60a3..22a016dc 100644 --- a/src/batch/src/Test/Factory/SequenceJobExecutionIdGenerator.php +++ b/src/batch/src/Test/Factory/SequenceJobExecutionIdGenerator.php @@ -6,6 +6,10 @@ use Yokai\Batch\Factory\JobExecutionIdGeneratorInterface; +/** + * This {@see JobExecutionIdGeneratorInterface} should be used in test to generate predictable ids. + * Sequence is provided at construction and items will be + */ final class SequenceJobExecutionIdGenerator implements JobExecutionIdGeneratorInterface { /** diff --git a/src/batch/src/Test/Job/Item/Processor/TestDebugProcessor.php b/src/batch/src/Test/Job/Item/Processor/TestDebugProcessor.php index bab99670..41667b9d 100644 --- a/src/batch/src/Test/Job/Item/Processor/TestDebugProcessor.php +++ b/src/batch/src/Test/Job/Item/Processor/TestDebugProcessor.php @@ -7,6 +7,11 @@ use Yokai\Batch\Job\Item\ItemProcessorInterface; use Yokai\Batch\Test\Job\Item\TestDebugComponent; +/** + * This {@see ItemProcessorInterface} should be used in test + * for components working with generic {@see ItemProcessorInterface}. + * It provides convenient assertion methods to ensure your processor was used correctly. + */ final class TestDebugProcessor extends TestDebugComponent implements ItemProcessorInterface { private ItemProcessorInterface $decorated; diff --git a/src/batch/src/Test/Job/Item/Reader/TestDebugReader.php b/src/batch/src/Test/Job/Item/Reader/TestDebugReader.php index ce84248a..c1fed391 100644 --- a/src/batch/src/Test/Job/Item/Reader/TestDebugReader.php +++ b/src/batch/src/Test/Job/Item/Reader/TestDebugReader.php @@ -7,6 +7,11 @@ use Yokai\Batch\Job\Item\ItemReaderInterface; use Yokai\Batch\Test\Job\Item\TestDebugComponent; +/** + * This {@see ItemReaderInterface} should be used in test + * for components working with generic {@see ItemReaderInterface}. + * It provides convenient assertion methods to ensure your reader was used correctly. + */ final class TestDebugReader extends TestDebugComponent implements ItemReaderInterface { private ItemReaderInterface $decorated; diff --git a/src/batch/src/Test/Job/Item/TestDebugComponent.php b/src/batch/src/Test/Job/Item/TestDebugComponent.php index a62ab192..533b14b7 100644 --- a/src/batch/src/Test/Job/Item/TestDebugComponent.php +++ b/src/batch/src/Test/Job/Item/TestDebugComponent.php @@ -15,6 +15,10 @@ use Yokai\Batch\JobParameters; use Yokai\Batch\Summary; +/** + * This base contains all "runtime" interfaces handled by the lib. + * It provides convenient assertion methods to ensure your component was used correctly. + */ abstract class TestDebugComponent implements InitializableInterface, FlushableInterface, @@ -36,6 +40,10 @@ public function __construct( ) { } + /** + * @internal + * Utility method to simulate configuration from a {@see JobExecution}. + */ public function configure(JobExecution $jobExecution): void { $this->setJobExecution($jobExecution); @@ -59,6 +67,12 @@ public function setSummary(Summary $summary): void $this->summaryProvided = true; } + /** + * Assert that component was provided with *Aware interfaces: + * - {@see JobExecutionAwareInterface} + * - {@see JobParametersAwareInterface} + * - {@see SummaryAwareInterface} + */ public function assertWasConfigured(): void { Assert::assertTrue($this->jobExecutionProvided, 'Job execution was configured'); @@ -66,6 +80,12 @@ public function assertWasConfigured(): void Assert::assertTrue($this->summaryProvided, 'Summary was configured'); } + /** + * Assert that component was not provided with *Aware interfaces: + * - {@see JobExecutionAwareInterface} + * - {@see JobParametersAwareInterface} + * - {@see SummaryAwareInterface} + */ public function assertWasNotConfigured(): void { Assert::assertFalse($this->jobExecutionProvided, 'Job execution was not configured'); @@ -86,6 +106,10 @@ public function flush(): void $this->flushElement($this->decorated); } + /** + * Assert that component was used (depends on extending class). + * Also ensure that component was initialized & flushed. + */ public function assertWasUsed(): void { Assert::assertTrue($this->initialized, 'Element was initialized'); @@ -93,6 +117,10 @@ public function assertWasUsed(): void Assert::assertTrue($this->flushed, 'Element was flushed'); } + /** + * Assert that component was not used (depends on extending class). + * Also ensure that component was (or was not) initialized & flushed. + */ public function assertWasNotUsed(bool $initialized = false, bool $flushed = false): void { Assert::assertSame( diff --git a/src/batch/src/Test/Job/Item/Writer/InMemoryWriter.php b/src/batch/src/Test/Job/Item/Writer/InMemoryWriter.php index 0e1a5d46..d4d3ff42 100644 --- a/src/batch/src/Test/Job/Item/Writer/InMemoryWriter.php +++ b/src/batch/src/Test/Job/Item/Writer/InMemoryWriter.php @@ -7,6 +7,12 @@ use Yokai\Batch\Job\Item\InitializableInterface; use Yokai\Batch\Job\Item\ItemWriterInterface; +/** + * This {@see ItemWriterInterface} should be used in test + * for components working with generic {@see ItemWriterInterface}. + * It provides convenient methods retrieve written items along execution + * and perform assertions on these. + */ final class InMemoryWriter implements ItemWriterInterface, InitializableInterface { /** diff --git a/src/batch/src/Test/Job/Item/Writer/TestDebugWriter.php b/src/batch/src/Test/Job/Item/Writer/TestDebugWriter.php index f267011f..e8077c2d 100644 --- a/src/batch/src/Test/Job/Item/Writer/TestDebugWriter.php +++ b/src/batch/src/Test/Job/Item/Writer/TestDebugWriter.php @@ -7,6 +7,11 @@ use Yokai\Batch\Job\Item\ItemWriterInterface; use Yokai\Batch\Test\Job\Item\TestDebugComponent; +/** + * This {@see ItemWriterInterface} should be used in test + * for components working with generic {@see ItemWriterInterface}. + * It provides convenient assertion methods to ensure your writer was used correctly. + */ final class TestDebugWriter extends TestDebugComponent implements ItemWriterInterface { private ItemWriterInterface $decorated; diff --git a/src/batch/src/Test/Launcher/BufferingJobLauncher.php b/src/batch/src/Test/Launcher/BufferingJobLauncher.php index f6e959fc..61dc100d 100644 --- a/src/batch/src/Test/Launcher/BufferingJobLauncher.php +++ b/src/batch/src/Test/Launcher/BufferingJobLauncher.php @@ -9,6 +9,11 @@ use Yokai\Batch\JobParameters; use Yokai\Batch\Launcher\JobLauncherInterface; +/** + * This {@see JobLauncherInterface} should be used in test. + * It will remember launched executions + * and will allow you to fetch these for assertions. + */ final class BufferingJobLauncher implements JobLauncherInterface { /** diff --git a/src/batch/src/Test/Storage/InMemoryJobExecutionStorage.php b/src/batch/src/Test/Storage/InMemoryJobExecutionStorage.php index 2bcdce02..e0b8e2ae 100644 --- a/src/batch/src/Test/Storage/InMemoryJobExecutionStorage.php +++ b/src/batch/src/Test/Storage/InMemoryJobExecutionStorage.php @@ -9,6 +9,11 @@ use Yokai\Batch\JobExecution; use Yokai\Batch\Storage\JobExecutionStorageInterface; +/** + * This {@see JobExecutionStorageInterface} should be used in test. + * It will store executions in memory + * and will allow you to fetch these for assertions. + */ final class InMemoryJobExecutionStorage implements JobExecutionStorageInterface { /** diff --git a/src/batch/src/Test/Storage/JobExecutionStorageTestTrait.php b/src/batch/src/Test/Storage/JobExecutionStorageTestTrait.php index 0ae9b1a5..21c09d51 100644 --- a/src/batch/src/Test/Storage/JobExecutionStorageTestTrait.php +++ b/src/batch/src/Test/Storage/JobExecutionStorageTestTrait.php @@ -7,6 +7,9 @@ use PHPUnit\Framework\Assert; use Yokai\Batch\JobExecution; +/** + * Handy methods for JobExecution storage test classes. + */ trait JobExecutionStorageTestTrait { private static function assertExecutions(array $expectedCouples, iterable $executions): void diff --git a/src/batch/src/Trigger/Scheduler/SchedulerInterface.php b/src/batch/src/Trigger/Scheduler/SchedulerInterface.php index 2335aab2..404b4d1a 100644 --- a/src/batch/src/Trigger/Scheduler/SchedulerInterface.php +++ b/src/batch/src/Trigger/Scheduler/SchedulerInterface.php @@ -12,6 +12,8 @@ interface SchedulerInterface { /** + * Get list of job to schedule. + * * @return ScheduledJob[] * @phpstan-return iterable */ diff --git a/src/batch/src/Trigger/Scheduler/TimeScheduler.php b/src/batch/src/Trigger/Scheduler/TimeScheduler.php index ae49182d..ad6b4498 100644 --- a/src/batch/src/Trigger/Scheduler/TimeScheduler.php +++ b/src/batch/src/Trigger/Scheduler/TimeScheduler.php @@ -10,7 +10,7 @@ /** * This scheduler implementation uses constructor settings to compute schedules. - * The main setting is a @see DateTimeInterface that will be converted to a closure, + * The main setting is a {@see DateTimeInterface} that will be converted to a closure, * if that date is before job execution start time, the associated job schedule will be triggered. * * Example : diff --git a/src/batch/src/Trigger/TriggerScheduledJobsJob.php b/src/batch/src/Trigger/TriggerScheduledJobsJob.php index 78bc7157..1e4833ca 100644 --- a/src/batch/src/Trigger/TriggerScheduledJobsJob.php +++ b/src/batch/src/Trigger/TriggerScheduledJobsJob.php @@ -11,7 +11,7 @@ /** * This job is able to automatically trigger other jobs when you decide it. - * It rely on a list of @see SchedulerInterface that tells this job what jobs to trigger. + * It rely on a list of {@see SchedulerInterface} that tells this job what jobs to trigger. * * This job can be launched using a crontab, so the jobs you scheduled will be evaluated at each crontab rotation. */ diff --git a/tests/convention/Autoload.php b/tests/convention/Autoload.php index ee8b0b95..f90914a0 100644 --- a/tests/convention/Autoload.php +++ b/tests/convention/Autoload.php @@ -36,20 +36,9 @@ public static function getFQCN(string $file): string } /** - * List all dirs where yokai batch packages are living. - * * @return iterable */ - public static function listPackageDirs(): iterable - { - $composer = \json_decode(\file_get_contents(__DIR__ . '/../../composer.json'), true); - - foreach ($composer['autoload']['psr-4'] as $dir) { - yield __DIR__ . '/../../' . $dir; - } - } - - private static function listFiles(string $path): iterable + public static function listFiles(string $path): iterable { $files = Finder::create()->files()->in($path)->name('*.php'); /** @var SplFileInfo $file */ @@ -62,7 +51,7 @@ private static function getNamespace(string $filename): string { \preg_match('/namespace (.*);/', \file_get_contents($filename), $namespace); - return $namespace[1] ?? throw new \Exception('No namespace'); + return $namespace[1] ?? throw new \Exception('No namespace in ' . $filename); } private static function getClassname(string $filename): string diff --git a/tests/convention/Comment/ClassCommentsTest.php b/tests/convention/Comment/ClassCommentsTest.php new file mode 100644 index 00000000..6a97d820 --- /dev/null +++ b/tests/convention/Comment/ClassCommentsTest.php @@ -0,0 +1,46 @@ +getDocComment(), + "{$class->getName()} must have comment." + ); + } + + /** + * @dataProvider classes + */ + public function testAllSeeDocAreSurroundedWithBrackets(ReflectionClass $class): void + { + self::assertAllSeeDocAreSurroundedWithBrackets((string)$class->getDocComment()); + } + + public function classes(): iterable + { + /** @var Package $package */ + foreach (Packages::listYokaiPackages() as $package) { + foreach (Autoload::listAllFQCN($package->sources()) as $class) { + if (\str_ends_with($class, 'Exception')) { + continue; + } + + yield $class => [new ReflectionClass($class)]; + } + } + } +} diff --git a/tests/convention/Comment/CommentsTestCase.php b/tests/convention/Comment/CommentsTestCase.php new file mode 100644 index 00000000..0724843d --- /dev/null +++ b/tests/convention/Comment/CommentsTestCase.php @@ -0,0 +1,23 @@ +getDocComment() ?: ''); + $lines = \array_map(fn ($line) => \trim($line, '/* '), $lines); + $lines = \array_filter($lines, fn ($line) => !\str_starts_with($line, '@')); + $lines = \array_filter($lines); + $comment = \implode(\PHP_EOL, $lines); + + self::assertNotEmpty( + $comment, + "{$this->methodFQCN($method)} is a public method and must have comment." . + " In {$this->fileAndLine($method)}." + ); + } + + /** + * @dataProvider publicMethods + */ + public function testAllSeeDocAreSurroundedWithBrackets(ReflectionMethod $method): void + { + self::assertAllSeeDocAreSurroundedWithBrackets((string)$method->getDocComment()); + } + + public function publicMethods(): iterable + { + $magicMethods = \array_fill_keys(['__construct', '__invoke'], true); + + /** @var Package $package */ + foreach (Packages::listYokaiPackages() as $package) { + foreach (Autoload::listAllFQCN($package->sources()) as $class) { + if (\str_ends_with($class, 'Exception')) { + continue; + } + + $class = new ReflectionClass($class); + + $methodsFromInterfaces = []; + foreach ($class->getInterfaces() as $interface) { + foreach ($interface->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + $methodsFromInterfaces[$method->getName()] = true; + } + } + $accessorMethods = []; + foreach ($class->getProperties() as $property) { + $accessorMethods[$property->getName()] = true; + $propertyName = \ucfirst($property->getName()); + foreach (['get', 'set', 'is'] as $prefix) { + $accessorMethods[$prefix . $propertyName] = true; + } + } + + foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->getDeclaringClass()->getName() !== $class->getName()) { + continue; + } + if (isset($magicMethods[$method->getName()])) { + // you should understand what magic methods are used for + continue; + } + if (isset($methodsFromInterfaces[$method->getName()])) { + // methods inherited from interface will have comment at interface level + continue; + } + if (isset($accessorMethods[$method->getName()])) { + // you should understand what accessor methods are used for + continue; + } + + yield $this->methodFQCN($method) => [$method]; + } + } + } + } + + private function methodFQCN(ReflectionMethod $method): string + { + return "{$method->getDeclaringClass()->getName()}::{$method->getName()}"; + } + + private function fileAndLine(ReflectionMethod $method): string + { + return "{$method->getDeclaringClass()->getFileName()}:{$method->getStartLine()}"; + } +} diff --git a/tests/convention/Composer.php b/tests/convention/Composer.php new file mode 100644 index 00000000..86115176 --- /dev/null +++ b/tests/convention/Composer.php @@ -0,0 +1,84 @@ +json = \json_decode(\file_get_contents($path), true); + } + + public function name(): string + { + return $this->json['name']; + } + + /** + * @return array + */ + public function replace(): array + { + return $this->json['replace'] ?? []; + } + + /** + * @return array + */ + public function require(): array + { + return $this->json['require'] ?? []; + } + + /** + * @return array + */ + public function suggest(): array + { + return \array_keys($this->json['suggest'] ?? []); + } + + /** + * @return array + */ + public function packages(): array + { + return \array_keys($this->require()); + } + + /** + * @return array + */ + public function autoload(): array + { + return $this->json['autoload']['psr-4'] ?? []; + } + + /** + * @return array + */ + public function requireDev(): array + { + return $this->json['require-dev'] ?? []; + } + + /** + * @return array + */ + public function packagesDev(): array + { + return \array_keys($this->requireDev()); + } + + /** + * @return array + */ + public function autoloadDev(): array + { + return $this->json['autoload-dev']['psr-4'] ?? []; + } +} diff --git a/tests/convention/Dependency/PackagesTest.php b/tests/convention/Dependency/PackagesTest.php new file mode 100644 index 00000000..931117ed --- /dev/null +++ b/tests/convention/Dependency/PackagesTest.php @@ -0,0 +1,82 @@ +namespace(), // Package can use itself + 'Composer\\', // Package can use Composer introspection + 'PHPUnit\\', // Package may provide test dummies in sources + ]; + foreach (\array_merge($package->composer->packages(), $package->composer->suggest()) as $require) { + if (!\str_contains($require, '/')) { + continue; // php & extensions + } + $requirePackage = Packages::getPackage($require); + foreach (\array_keys($requirePackage->composer->autoload()) as $prefix) { + $requirePrefixes[] = $prefix; + } + } + + foreach (Autoload::listFiles($package->sources()) as $path) { + \preg_match_all('/^use ([^;]+\\\\[^;]+);$/m', \file_get_contents($path), $matches); + foreach ($matches[1] as $use) { + foreach ($requirePrefixes as $prefix) { + if (\str_starts_with($use, $prefix)) { + self::assertTrue(true); + continue 2; + } + } + self::fail(Autoload::getFQCN($path) . ' is using ' . $use . ' which is not in a required package'); + } + } + } + + /** + * If packages are referencing same dependencies, versions should be same. + */ + public function testPackagesAreUsingSameDependencyVersions(): void + { + $requirements = []; + /** @var Package $package */ + foreach (Packages::listYokaiPackages() as $package) { + foreach ($package->composer->require() as $dependency => $version) { + $requirements[$dependency] ??= []; + $requirements[$dependency][] = $version; + } + } + + foreach ($requirements as $dependency => $versions) { + $distinctVersions = \array_values(\array_unique($versions)); + self::assertCount( + 1, + $distinctVersions, + "{$dependency} is configured on the same version : " . \json_encode($distinctVersions) + ); + } + } + + public function packages(): iterable + { + /** @var Package $package */ + foreach (Packages::listYokaiPackages() as $package) { + yield $package->name => [$package]; + } + } +} diff --git a/tests/convention/Dependency/SourcesTest.php b/tests/convention/Dependency/SourcesTest.php new file mode 100644 index 00000000..a72c96af --- /dev/null +++ b/tests/convention/Dependency/SourcesTest.php @@ -0,0 +1,84 @@ + Path::makeRelative($path, __DIR__ . '/../../..') . '/'; + + $expectedReplace = []; + $expectedProdDeps = []; + $expectedDevDeps = []; + $expectedProdAutoload = []; + $expectedDevAutoload = []; + /** @var Package $package */ + foreach (Packages::listYokaiPackages() as $package) { + $expectedProdDeps = \array_merge($expectedProdDeps, $package->composer->packages()); + $expectedDevDeps = \array_merge($expectedDevDeps, $package->composer->packagesDev()); + $expectedReplace[$package->name] = 'self.version'; + $expectedProdAutoload[$package->namespace()] = $relative($package->sources()); + $expectedDevAutoload[$package->testsNamespace()] = $relative($package->tests()); + } + + $rootComposer = Packages::getRootComposer(); + + $expectedProdDeps = \array_unique($expectedProdDeps); + \sort($expectedProdDeps); + $prodDeps = $rootComposer->packages(); + $prodDeps[] = 'yokai/batch'; + \sort($prodDeps); + self::assertEmpty( + \array_diff($expectedProdDeps, $prodDeps), + 'Dependencies of all packages are required in root composer.json' + ); + + $expectedDevDeps = \array_unique($expectedDevDeps); + \sort($expectedDevDeps); + $devDeps = $rootComposer->packagesDev(); + \sort($devDeps); + self::assertEmpty( + \array_diff($expectedDevDeps, $devDeps), + 'Dev dependencies of all packages are required in root composer.json' + ); + + $replace = $rootComposer->replace(); + \ksort($replace); + \ksort($expectedReplace); + self::assertSame( + $expectedReplace, + $replace, + 'All packages are replaced in root composer.json' + ); + + $prodAutoload = $rootComposer->autoload(); + \ksort($prodAutoload); + \ksort($expectedProdAutoload); + self::assertEmpty( + \array_diff($expectedProdAutoload, $prodAutoload), + 'All packages autoload rules are duplicated in root composer.json' + ); + + $devAutoload = $rootComposer->autoloadDev(); + \ksort($devAutoload); + \ksort($expectedDevAutoload); + self::assertEmpty( + \array_diff($expectedDevAutoload, $devAutoload), + 'All packages dev autoload rules are duplicated in root composer.json' + ); + } +} diff --git a/tests/convention/Documentation/MarkdownLinksTest.php b/tests/convention/Documentation/MarkdownLinksTest.php index 165777af..ebc76b00 100644 --- a/tests/convention/Documentation/MarkdownLinksTest.php +++ b/tests/convention/Documentation/MarkdownLinksTest.php @@ -12,6 +12,8 @@ use Yokai\Batch\Job\Parameters\JobParameterAccessorInterface; use Yokai\Batch\Launcher\JobLauncherInterface; use Yokai\Batch\Sources\Tests\Convention\Autoload; +use Yokai\Batch\Sources\Tests\Convention\Package; +use Yokai\Batch\Sources\Tests\Convention\Packages; use Yokai\Batch\Storage\JobExecutionStorageInterface; /** @@ -66,8 +68,9 @@ public function testComponentsAreListed(string $filepath, string $interface): vo $actualClasses = []; // Find all classes in libraries that implement this interface - foreach (Autoload::listPackageDirs() as $dir) { - foreach (Autoload::listAllFQCN($dir) as $class) { + /** @var Package $package */ + foreach (Packages::listYokaiPackages() as $package) { + foreach (Autoload::listAllFQCN($package->sources()) as $class) { if ($class !== $interface && \is_a($class, $interface, true)) { $expectedClasses[] = $class; } diff --git a/tests/convention/Package.php b/tests/convention/Package.php new file mode 100644 index 00000000..d516d2f8 --- /dev/null +++ b/tests/convention/Package.php @@ -0,0 +1,62 @@ +composer = new Composer($path . '/composer.json'); + $this->name = $this->composer->name(); + } + + /** + * Absolute dir where sources lives. + */ + public function sources(): string + { + return \realpath($this->path . '/' . \array_values($this->composer->autoload())[0]); + } + + /** + * Namespace prefix of sources. + */ + public function namespace(): string + { + return \array_keys($this->composer->autoload())[0]; + } + + /** + * Absolute dir where tests lives. + */ + public function tests(): string + { + return \realpath($this->path . '/' . \array_values($this->composer->autoloadDev())[0]); + } + + /** + * Namespace prefix of tests. + */ + public function testsNamespace(): string + { + return \array_keys($this->composer->autoloadDev())[0]; + } +} diff --git a/tests/convention/Packages.php b/tests/convention/Packages.php new file mode 100644 index 00000000..fd5d9621 --- /dev/null +++ b/tests/convention/Packages.php @@ -0,0 +1,43 @@ + + */ + public static function listYokaiPackages(): iterable + { + foreach (self::getRootComposer()->autoload() as $dir) { + yield new Package(\dirname(__DIR__ . '/../../' . $dir)); + } + } + + /** + * Create a package object based on the name (from sources or vendor). + */ + public static function getPackage(string $name): Package + { + if (\str_starts_with($name, 'yokai/')) { + return new Package(__DIR__ . '/../../src/' . \explode('/', $name)[1]); + } + + return new Package(__DIR__ . '/../../vendor/' . $name); + } + + /** + * Build object from composer.json in root dir. + */ + public static function getRootComposer(): Composer + { + return new Composer(__DIR__ . '/../../composer.json'); + } +}