diff --git a/composer.json b/composer.json index 8f75f952a..5594c6ef8 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "monolog/monolog": "^2.0||^3.0", "packaged/thrift": "^0.15.0", "php-http/discovery": "^1.0", + "psr/clock": "^1.0", "psr/http-client": "^1.0", "psr/http-message": "^1.0 || ^2.0", "psr/log": "^2.0 || ^3.0", diff --git a/composer.lock b/composer.lock index c1a2f6171..f512faea7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f116e7d09bc3a52f8d3288b08b9a263e", + "content-hash": "8400ea2b77c9a64e8896d8bf98426259", "packages": [ { "name": "aeon-php/calendar", @@ -1937,6 +1937,54 @@ }, "time": "2021-02-03T23:26:27+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -5224,7 +5272,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -5240,6 +5288,6 @@ "ext-zlib": "*", "composer-runtime-api": "^2.1" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/src/core/etl/composer.json b/src/core/etl/composer.json index 52e4207f4..aa15173c9 100644 --- a/src/core/etl/composer.json +++ b/src/core/etl/composer.json @@ -12,6 +12,7 @@ "php": "~8.2.0 || ~8.3.0 || ~8.4.0", "ext-json": "*", "ext-mbstring": "*", + "psr/clock": "^1.0", "flow-php/array-dot": "^0.10.0 || 1.x-dev", "flow-php/rdsl": "^0.10.0 || 1.x-dev", "flow-php/filesystem": "^0.10.0 || 1.x-dev", diff --git a/src/core/etl/src/Flow/Clock/FakeClock.php b/src/core/etl/src/Flow/Clock/FakeClock.php new file mode 100644 index 000000000..af8e3f834 --- /dev/null +++ b/src/core/etl/src/Flow/Clock/FakeClock.php @@ -0,0 +1,29 @@ +dateTime = $this->dateTime->modify($modify); + } + + public function now() : \DateTimeImmutable + { + return $this->dateTime; + } + + public function set(\DateTimeImmutable $dateTime) : void + { + $this->dateTime = $dateTime; + } +} diff --git a/src/core/etl/src/Flow/Clock/SystemClock.php b/src/core/etl/src/Flow/Clock/SystemClock.php new file mode 100644 index 000000000..c54d6ac4f --- /dev/null +++ b/src/core/etl/src/Flow/Clock/SystemClock.php @@ -0,0 +1,29 @@ +timezone); + } +} diff --git a/src/core/etl/src/Flow/ETL/Config.php b/src/core/etl/src/Flow/ETL/Config.php index 824e34b3d..9a872dadf 100644 --- a/src/core/etl/src/Flow/ETL/Config.php +++ b/src/core/etl/src/Flow/ETL/Config.php @@ -13,6 +13,7 @@ use Flow\ETL\Row\EntryFactory; use Flow\Filesystem\{FilesystemTable}; use Flow\Serializer\Serializer; +use Psr\Clock\ClockInterface; /** * Immutable configuration that can be used to initialize many contexts. @@ -33,6 +34,7 @@ public function __construct( private string $id, private Serializer $serializer, + private ClockInterface $clock, private FilesystemTable $filesystemTable, private FilesystemStreams $filesystemStreams, private Optimizer $optimizer, @@ -59,6 +61,11 @@ public function caster() : Caster return $this->caster; } + public function clock() : ClockInterface + { + return $this->clock; + } + public function entryFactory() : EntryFactory { return $this->entryFactory; diff --git a/src/core/etl/src/Flow/ETL/Config/ConfigBuilder.php b/src/core/etl/src/Flow/ETL/Config/ConfigBuilder.php index 879fdf95a..d0ecf252b 100644 --- a/src/core/etl/src/Flow/ETL/Config/ConfigBuilder.php +++ b/src/core/etl/src/Flow/ETL/Config/ConfigBuilder.php @@ -5,6 +5,7 @@ namespace Flow\ETL\Config; use function Flow\Filesystem\DSL\fstab; +use Flow\Clock\SystemClock; use Flow\ETL\Config\Cache\CacheConfigBuilder; use Flow\ETL\Config\Sort\SortConfigBuilder; use Flow\ETL\Filesystem\FilesystemStreams; @@ -15,6 +16,7 @@ use Flow\ETL\{Cache, Config, NativePHPRandomValueGenerator, RandomValueGenerator}; use Flow\Filesystem\{Filesystem, FilesystemTable}; use Flow\Serializer\{Base64Serializer, NativePHPSerializer, Serializer}; +use Psr\Clock\ClockInterface; final class ConfigBuilder { @@ -24,6 +26,8 @@ final class ConfigBuilder private ?Caster $caster; + private ?ClockInterface $clock; + private ?FilesystemTable $fstab; private ?string $id; @@ -44,6 +48,7 @@ public function __construct() $this->putInputIntoRows = false; $this->optimizer = null; $this->caster = null; + $this->clock = null; $this->cache = new CacheConfigBuilder(); $this->sort = new SortConfigBuilder(); $this->randomValueGenerator = new NativePHPRandomValueGenerator(); @@ -54,7 +59,7 @@ public function build() : Config $this->id ??= 'flow_php' . $this->randomValueGenerator->string(32); $entryFactory = new NativeEntryFactory(); $this->serializer ??= new Base64Serializer(new NativePHPSerializer()); - + $this->clock ??= SystemClock::utc(); $this->optimizer ??= new Optimizer( new Optimizer\LimitOptimization(), new Optimizer\BatchSizeOptimization(batchSize: 1000) @@ -65,6 +70,7 @@ public function build() : Config return new Config( $this->id, $this->serializer, + $this->clock, $this->fstab(), new FilesystemStreams($this->fstab()), $this->optimizer, @@ -83,6 +89,13 @@ public function cache(Cache $cache) : self return $this; } + public function clock(ClockInterface $clocks) : self + { + $this->clock = $clocks; + + return $this; + } + public function dontPutInputIntoRows() : self { $this->putInputIntoRows = false; diff --git a/src/core/etl/src/Flow/ETL/DataFrame.php b/src/core/etl/src/Flow/ETL/DataFrame.php index 7bf1888af..a00fc5921 100644 --- a/src/core/etl/src/Flow/ETL/DataFrame.php +++ b/src/core/etl/src/Flow/ETL/DataFrame.php @@ -745,7 +745,7 @@ public function rows(Transformer|Transformation $transformer) : self /** * @trigger * - * @param null|callable(Rows $rows): void $callback + * @param null|callable(Rows $rows, FlowContext $context): void $callback * @param bool $analyze - when set to true, run will return Report */ #[DSLMethod(exclude: true)] @@ -756,9 +756,13 @@ public function run(?callable $callback = null, bool $analyze = false) : ?Report $totalRows = 0; $schema = new Schema(); + if ($analyze) { + $startedAt = $this->context->config->clock()->now(); + } + foreach ($clone->pipeline->process($clone->context) as $rows) { if ($callback !== null) { - $callback($rows); + $callback($rows, $clone->context); } if ($analyze) { @@ -768,7 +772,9 @@ public function run(?callable $callback = null, bool $analyze = false) : ?Report } if ($analyze) { - return new Report($schema, new Statistics($totalRows)); + $endedAt = $this->context->config->clock()->now(); + + return new Report($schema, new Statistics($totalRows, new Statistics\ExecutionTime($startedAt, $endedAt))); } return null; diff --git a/src/core/etl/src/Flow/ETL/Dataset/Statistics.php b/src/core/etl/src/Flow/ETL/Dataset/Statistics.php index 728850afe..24d87b68e 100644 --- a/src/core/etl/src/Flow/ETL/Dataset/Statistics.php +++ b/src/core/etl/src/Flow/ETL/Dataset/Statistics.php @@ -4,10 +4,13 @@ namespace Flow\ETL\Dataset; +use Flow\ETL\Dataset\Statistics\ExecutionTime; + final readonly class Statistics { public function __construct( private int $totalRows, + public readonly ExecutionTime $executionTime, ) { } diff --git a/src/core/etl/src/Flow/ETL/Dataset/Statistics/ExecutionTime.php b/src/core/etl/src/Flow/ETL/Dataset/Statistics/ExecutionTime.php new file mode 100644 index 000000000..53b2ff003 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/Dataset/Statistics/ExecutionTime.php @@ -0,0 +1,27 @@ + $finishedAt) { + throw new InvalidArgumentException('Execution start date must be before finish date'); + } + } + + public function duration() : \DateInterval + { + return $this->startedAt->diff($this->finishedAt); + } + + public function inSeconds() : int + { + return $this->finishedAt->getTimestamp() - $this->startedAt->getTimestamp(); + } +} diff --git a/src/core/etl/tests/Flow/Clock/SystemClockTest.php b/src/core/etl/tests/Flow/Clock/SystemClockTest.php new file mode 100644 index 000000000..bc1aba268 --- /dev/null +++ b/src/core/etl/tests/Flow/Clock/SystemClockTest.php @@ -0,0 +1,31 @@ +now()); + } + + public function test_system_clock() : void + { + $clock = SystemClock::system(); + + self::assertInstanceOf(SystemClock::class, $clock); + } + + public function test_utc_clock() : void + { + $clock = SystemClock::utc(); + + self::assertInstanceOf(SystemClock::class, $clock); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Integration/DataFrame/AnalyzeTest.php b/src/core/etl/tests/Flow/ETL/Tests/Integration/DataFrame/AnalyzeTest.php index 8c5493eaf..f6fc063af 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Integration/DataFrame/AnalyzeTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Integration/DataFrame/AnalyzeTest.php @@ -5,14 +5,27 @@ namespace Flow\ETL\Tests\Integration\DataFrame; use function Flow\ETL\Adapter\Text\from_text; -use function Flow\ETL\DSL\{datetime_schema, df, float_schema, from_array, int_schema, schema, str_schema}; +use function Flow\ETL\DSL\{ + config_builder, + datetime_schema, + df, + float_schema, + from_array, + int_schema, + schema, + str_schema}; +use Flow\Clock\FakeClock; use Flow\ETL\Tests\FlowIntegrationTestCase; +use Flow\ETL\{FlowContext, Rows}; final class AnalyzeTest extends FlowIntegrationTestCase { public function test_analyzing_csv_file_with_auto_cast() : void { - $report = df() + $config = config_builder()->clock($clock = new FakeClock())->build(); + + $clock->set(new \DateTimeImmutable('2025-01-01 00:00:00 UTC')); + $report = df($config) ->read(from_array([ ['Index' => 1, 'Date' => '2024-01-19', 'Close' => '2029.3', 'Volume' => '166078.0', 'Open' => '2027.4', 'High' => '2041.9', 'Low' => '2022.2'], ['Index' => 2, 'Date' => '2024-01-20', 'Close' => '2029.3', 'Volume' => '166078.0', 'Open' => '2027.4', 'High' => '2041.9', 'Low' => '2022.2'], @@ -21,7 +34,15 @@ public function test_analyzing_csv_file_with_auto_cast() : void ['Index' => 5, 'Date' => '2024-01-23', 'Close' => '2029.3', 'Volume' => '166078.0', 'Open' => '2027.4', 'High' => '2041.9', 'Low' => '2022.2'], ])) ->autoCast() - ->run(analyze: true); + ->collect() + ->run(function (Rows $rows, FlowContext $context) : void { + $clock = $context->config->clock(); + + if ($clock instanceof FakeClock) { + $clock->modify('+5 minutes'); + } + + }, analyze: true); self::assertNotNull($report); self::assertSame(5, $report->statistics()->totalRows()); @@ -38,6 +59,10 @@ public function test_analyzing_csv_file_with_auto_cast() : void $report->schema() ); self::assertSame(7, $report->schema()->count()); + self::assertInstanceOf(\DateTimeImmutable::class, $report->statistics()->executionTime->startedAt); + self::assertInstanceOf(\DateTimeImmutable::class, $report->statistics()->executionTime->finishedAt); + self::assertGreaterThanOrEqual($report->statistics()->executionTime->startedAt, $report->statistics()->executionTime->finishedAt); + self::assertEquals(5 * 60, $report->statistics()->executionTime->inSeconds()); } public function test_analyzing_csv_file_with_limit() : void diff --git a/tools/blackfire/composer.lock b/tools/blackfire/composer.lock index 0d473a33a..32f2ba860 100644 --- a/tools/blackfire/composer.lock +++ b/tools/blackfire/composer.lock @@ -162,10 +162,10 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/tools/box/composer.lock b/tools/box/composer.lock index bb9262f0b..758e89b12 100644 --- a/tools/box/composer.lock +++ b/tools/box/composer.lock @@ -1395,16 +1395,16 @@ }, { "name": "humbug/php-scoper", - "version": "0.18.15", + "version": "0.18.16", "source": { "type": "git", "url": "https://github.com/humbug/php-scoper.git", - "reference": "79b2b4e0fbc1d1ef6ae99c4e078137b42bd43d19" + "reference": "aff0ef968d8a07ea5be8a2fe797dbf4927e750af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/humbug/php-scoper/zipball/79b2b4e0fbc1d1ef6ae99c4e078137b42bd43d19", - "reference": "79b2b4e0fbc1d1ef6ae99c4e078137b42bd43d19", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/aff0ef968d8a07ea5be8a2fe797dbf4927e750af", + "reference": "aff0ef968d8a07ea5be8a2fe797dbf4927e750af", "shasum": "" }, "require": { @@ -1425,7 +1425,7 @@ "fidry/makefile": "^1.0", "humbug/box": "^4.6.2", "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^10.0", + "phpunit/phpunit": "^10.0 || ^11.0", "symfony/yaml": "^6.4 || ^7.0" }, "bin": [ @@ -1473,9 +1473,9 @@ "description": "Prefixes all PHP namespaces in a file or directory.", "support": { "issues": "https://github.com/humbug/php-scoper/issues", - "source": "https://github.com/humbug/php-scoper/tree/0.18.15" + "source": "https://github.com/humbug/php-scoper/tree/0.18.16" }, - "time": "2024-09-02T13:35:10+00:00" + "time": "2025-01-12T15:48:25+00:00" }, { "name": "jetbrains/phpstorm-stubs", diff --git a/tools/cs-fixer/composer.lock b/tools/cs-fixer/composer.lock index 51c3ef5b9..472e3bca0 100644 --- a/tools/cs-fixer/composer.lock +++ b/tools/cs-fixer/composer.lock @@ -2537,10 +2537,10 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/tools/phpunit/composer.lock b/tools/phpunit/composer.lock index a5d2eea26..7cd8fdd7f 100644 --- a/tools/phpunit/composer.lock +++ b/tools/phpunit/composer.lock @@ -566,16 +566,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.40", + "version": "10.5.41", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e6ddda95af52f69c1e0c7b4f977cccb58048798c" + "reference": "e76586fa3d49714f230221734b44892e384109d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e6ddda95af52f69c1e0c7b4f977cccb58048798c", - "reference": "e6ddda95af52f69c1e0c7b4f977cccb58048798c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e76586fa3d49714f230221734b44892e384109d7", + "reference": "e76586fa3d49714f230221734b44892e384109d7", "shasum": "" }, "require": { @@ -647,7 +647,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.40" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.41" }, "funding": [ { @@ -663,7 +663,7 @@ "type": "tidelift" } ], - "time": "2024-12-21T05:49:06+00:00" + "time": "2025-01-13T09:33:05+00:00" }, { "name": "sebastian/cli-parser",