From 95513316b529bc6a894b32ca336a532e83837cf4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 11:40:23 -0400 Subject: [PATCH 01/32] Update phpstan/phpstan requirement from 1.10.32 to 1.10.33 (#452) Updates the requirements on [phpstan/phpstan](https://github.com/phpstan/phpstan) to permit the latest version. - [Release notes](https://github.com/phpstan/phpstan/releases) - [Changelog](https://github.com/phpstan/phpstan/blob/1.11.x/CHANGELOG.md) - [Commits](https://github.com/phpstan/phpstan/compare/1.10.32...1.10.33) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 627f8558..1b14130d 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "mockery/mockery": "^1.6.6", "php-stubs/wp-cli-stubs": "^2.8", "phpstan/phpdoc-parser": "^1.23.1", - "phpstan/phpstan": "1.10.32", + "phpstan/phpstan": "1.10.33", "phpunit/phpunit": "^9.6.10", "predis/predis": "^2.2.0", "squizlabs/php_codesniffer": "^3.7", From 9b63dde8cc7a4e6c727678b149d9d569836a1584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 11:40:32 -0400 Subject: [PATCH 02/32] Bump actions/checkout from 3 to 4 (#451) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/split_monorepo.yml | 4 ++-- .github/workflows/update-changelog.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/split_monorepo.yml b/.github/workflows/split_monorepo.yml index a3a820ab..9092420c 100644 --- a/.github/workflows/split_monorepo.yml +++ b/.github/workflows/split_monorepo.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # required for matrix of packages set - uses: shivammathur/setup-php@v2 @@ -42,7 +42,7 @@ jobs: package: ${{fromJson(needs.provide_packages_json.outputs.matrix)}} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # no tag - diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index b20f3b6f..94df2c33 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: main From b642d8e06339c61b1ea673d831d3b630ec7bfd21 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Wed, 13 Sep 2023 11:40:42 -0400 Subject: [PATCH 03/32] Ensure that attribute and action methods are deduplicated (#447) * Ensure that attribute and action methods are deduplicated * Remove a legacy attribute version check * Add light tests for typehinting events to ease my mind --- .../events/class-discover-events.php | 33 +++++----- .../support/attributes/class-action.php | 6 +- src/mantle/support/class-service-provider.php | 66 +++++++++++++------ .../test-wordpress-action-dispatcher.php | 20 ++++++ tests/support/test-service-provider.php | 47 ++++++++++--- 5 files changed, 122 insertions(+), 50 deletions(-) diff --git a/src/mantle/framework/events/class-discover-events.php b/src/mantle/framework/events/class-discover-events.php index 7dafadfe..4c3b695f 100644 --- a/src/mantle/framework/events/class-discover-events.php +++ b/src/mantle/framework/events/class-discover-events.php @@ -20,8 +20,6 @@ /** * Discover events within a specific directory. - * - * @todo Add support for WordPress hooks using attributes. */ class Discover_Events { /** @@ -80,23 +78,22 @@ protected static function get_listener_events( $listeners, string $base_path ): } foreach ( $listener->getMethods( ReflectionMethod::IS_PUBLIC ) as $method ) { - // Check for attribute support with PHP 8. - if ( version_compare( phpversion(), '8.0.0', '>=' ) ) { - // Check if the method has an attribute action. - $action_attributes = $method->getAttributes( Action::class ); - - if ( ! empty( $action_attributes ) ) { - foreach ( $action_attributes as $attribute ) { - $instance = $attribute->newInstance(); - - $listener_events[ $listener->name . '@' . $method->name ] = [ - [ $instance->action ], - $instance->priority, - ]; - } - - continue; + // Check if the method has an attribute action. + $action_attributes = $method->getAttributes( Action::class ); + + if ( ! empty( $action_attributes ) ) { + foreach ( $action_attributes as $attribute ) { + $instance = $attribute->newInstance(); + + $listener_events[ $listener->name . '@' . $method->name ] = [ + [ + $instance->hook_name, + ], + $instance->priority, + ]; } + + continue; } // Handle WordPress hooks being registered with a listener. diff --git a/src/mantle/support/attributes/class-action.php b/src/mantle/support/attributes/class-action.php index 57ab06e5..ead14ada 100644 --- a/src/mantle/support/attributes/class-action.php +++ b/src/mantle/support/attributes/class-action.php @@ -12,15 +12,15 @@ /** * Hook Action Attribute * - * Used to hook a method to an WordPress action at a specific priority. + * Used to hook a method to an WordPress hook at a specific priority. */ #[Attribute] class Action { /** * Constructor. * - * @param string $action Action name. + * @param string $hook_name Hook name. * @param int $priority Priority, defaults to 10. */ - public function __construct( public string $action, public int $priority = 10 ) {} + public function __construct( public string $hook_name, public int $priority = 10 ) {} } diff --git a/src/mantle/support/class-service-provider.php b/src/mantle/support/class-service-provider.php index 33b94a1d..c6676f0e 100644 --- a/src/mantle/support/class-service-provider.php +++ b/src/mantle/support/class-service-provider.php @@ -80,24 +80,36 @@ public function boot_provider() { } $this->boot_action_hooks(); - $this->boot_attribute_hooks(); $this->boot(); } /** - * Boot all actions on the service provider. + * Boot all actions and attribute methods on the service provider. * - * Allow methods in the 'on_{hook}_at_priority' and 'on_{hook}' format - * to automatically register WordPress hooks. + * Collects all of the `on_{hook}` and `on_{hook}_at_{priority}` methods as + * well as the attribute based `#[Action]` methods and registers them with + * the respective WordPress hooks. */ - protected function boot_action_hooks() { - collect( get_class_methods( static::class ) ) + protected function boot_action_hooks(): void { + $this->collect_action_methods() + ->merge( $this->collect_attribute_hooks() ) + ->unique() + ->each( + fn ( array $item ) => add_action( $item['hook'], [ $this, $item['method'] ], $item['priority'] ), + ); + } + + /** + * Collect all action methods from the service provider. + * + * @return Collection + */ + protected function collect_action_methods(): Collection { + return collect( get_class_methods( static::class ) ) ->filter( - function( string $method ) { - return Str::starts_with( $method, 'on_' ); - } + fn ( string $method ) => Str::starts_with( $method, 'on_' ) ) - ->each( + ->map( function( string $method ) { $hook = Str::after( $method, 'on_' ); $priority = 10; @@ -108,30 +120,42 @@ function( string $method ) { $hook = Str::before_last( $hook, '_at_' ); } - add_action( $hook, [ $this, $method ], $priority ); + return [ + 'hook' => $hook, + 'method' => $method, + 'priority' => $priority, + ]; } ); } /** - * Boot all attribute actions on the service provider. + * Collect all attribute actions on the service provider. + * + * Allow methods with the `#[Action]` attribute to automatically register + * WordPress hooks. + * + * @return Collection */ - protected function boot_attribute_hooks() { + protected function collect_attribute_hooks(): Collection { + $items = new Collection(); $class = new ReflectionClass( static::class ); foreach ( $class->getMethods() as $method ) { - $action_attributes = $method->getAttributes( Action::class ); - - if ( empty( $action_attributes ) ) { - continue; - } - - foreach ( $action_attributes as $attribute ) { + foreach ( $method->getAttributes( Action::class ) as $attribute ) { $instance = $attribute->newInstance(); - add_action( $instance->action, [ $this, $method->name ], $instance->priority ); + $items->push( + [ + 'hook' => $instance->hook_name, + 'method' => $method->getName(), + 'priority' => $instance->priority, + ] + ); } } + + return $items; } /** diff --git a/tests/events/test-wordpress-action-dispatcher.php b/tests/events/test-wordpress-action-dispatcher.php index 9abc88dc..297e0cea 100644 --- a/tests/events/test-wordpress-action-dispatcher.php +++ b/tests/events/test-wordpress-action-dispatcher.php @@ -170,4 +170,24 @@ function() { do_action( 'test_action_to_fire' ); $this->assertTrue( $_SERVER['__test'] ); } + + public function test_typehint_action_argument() { + $_SERVER['__test'] = false; + + add_action( + Example_Typehint_Event::class, + function ( Example_Typehint_Event $event ): void { + $_SERVER['__test'] = $event; + } + ); + + $this->app['events']->dispatch( new Example_Typehint_Event( 'test' ) ); + + $this->assertInstanceOf( Example_Typehint_Event::class, $_SERVER['__test'] ); + $this->assertEquals( 'test', $_SERVER['__test']->example ); + } +} + +class Example_Typehint_Event { + public function __construct( public string $example ) {} } diff --git a/tests/support/test-service-provider.php b/tests/support/test-service-provider.php index 3ea82d49..8b42d9c8 100644 --- a/tests/support/test-service-provider.php +++ b/tests/support/test-service-provider.php @@ -4,6 +4,7 @@ use Mantle\Application\Application; use Mantle\Console\Command; use Mantle\Contracts\Providers as ProviderContracts; +use Mantle\Events\Dispatcher; use Mantle\Support\Service_Provider; use Mantle\Support\Attributes\Action; use Mockery as m; @@ -14,6 +15,7 @@ protected function setUp(): void { remove_all_actions( 'init' ); remove_all_filters( 'custom_filter' ); + remove_all_filters( 'custom_filter_dedupe' ); Service_Provider::$publishes = []; Service_Provider::$publish_tags = []; @@ -66,12 +68,6 @@ public function test_hook_method_filter() { } public function test_hook_attribute() { - // Abandon if we're not running PHP 8. - if ( version_compare( phpversion(), '8.0.0', '<' ) ) { - $this->markTestSkipped( 'Requires PHP 8.0.0 or greater.' ); - return; - } - $app = m::mock( Application::class )->makePartial(); $app->register( Provider_Test_Hook::class ); $app->boot(); @@ -81,6 +77,30 @@ public function test_hook_attribute() { $this->assertTrue( $_SERVER['__custom_hook_fired'] ?? false ); } + public function test_hook_attribute_deduplicate() { + $app = m::mock( Application::class )->makePartial(); + $app->register( Provider_Test_Hook::class ); + $app->boot(); + + $value = apply_filters( 'custom_filter_dedupe', 0 ); + + $this->assertEquals( 10, $value ); + } + + public function test_typehint_event() { + $_SERVER['__custom_event_fired'] = false; + + $app = m::mock( Application::class )->makePartial(); + $app->register( Provider_Test_Hook::class ); + $app->boot(); + + $app['events'] = new Dispatcher( $app ); + + $app['events']->dispatch( new Example_Service_Provider_Event() ); + + $this->assertInstanceOf( Example_Service_Provider_Event::class, $_SERVER['__custom_event_fired'] ); + } + public function test_publishable_service_providers() { $app = m::mock( Application::class )->makePartial(); $app->register( ServiceProviderForTestingOne::class ); @@ -248,8 +268,15 @@ public function handle_custom_hook() { $_SERVER['__custom_hook_fired'] = true; } - public function handle_custom_filter( $value ) { - return $value + 100; + // Assert that only a single action is registered for this hook. + #[Action('custom_filter_dedupe')] + public function on_custom_filter_dedupe( $value ) { + return $value + 10; + } + + #[Action(Example_Service_Provider_Event::class)] + public function handle_custom_event( Example_Service_Provider_Event $event ) { + $_SERVER['__custom_event_fired'] = $event; } } @@ -270,3 +297,7 @@ public function boot() { $this->publishes( [ 'source/tagged/two/b' => 'destination/tagged/two/b' ], 'some_tag' ); } } + +class Example_Service_Provider_Event { + +} From d96e8bc9609abe58b0efcb17518d00eb770dec50 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Wed, 13 Sep 2023 16:43:41 -0400 Subject: [PATCH 04/32] WIP --- .../query/class-post-query-builder.php | 4 +- .../query/concerns/trait-queries-dates.php | 242 ++++++++++++++++++ .../query/test-post-query-builder.php | 56 ++++ 3 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 src/mantle/database/query/concerns/trait-queries-dates.php diff --git a/src/mantle/database/query/class-post-query-builder.php b/src/mantle/database/query/class-post-query-builder.php index 08f2158f..74e6dfc2 100644 --- a/src/mantle/database/query/class-post-query-builder.php +++ b/src/mantle/database/query/class-post-query-builder.php @@ -9,6 +9,7 @@ namespace Mantle\Database\Query; use Mantle\Database\Model\Term; +use Mantle\Database\Query\Concerns\Queries_Dates; use Mantle\Support\Helpers; use Mantle\Support\Collection; use WP_Term; @@ -30,7 +31,7 @@ * @method \Mantle\Database\Query\Post_Query_Builder whereType( string $type ) */ class Post_Query_Builder extends Builder { - use Queries_Relationships; + use Queries_Dates, Queries_Relationships; /** * Query Variable Aliases @@ -121,6 +122,7 @@ public function get_query_args(): array { 'suppress_filters' => false, 'tax_query' => $this->tax_query, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query ], + $this->get_date_query_args(), $this->wheres, ); } diff --git a/src/mantle/database/query/concerns/trait-queries-dates.php b/src/mantle/database/query/concerns/trait-queries-dates.php new file mode 100644 index 00000000..ff5e7afd --- /dev/null +++ b/src/mantle/database/query/concerns/trait-queries-dates.php @@ -0,0 +1,242 @@ +> + */ + protected array $date_constraints = []; + + /** + * The valid comparison operators for a date query. + * + * @var array + */ + protected array $date_operators = [ + '=', + '!=', + '>', + '>=', + '<', + '<=', + ]; + + /** + * Add a date query for a specific date to the query. + * + * @param DateTimeInterface|int|string $date + * @param string $compare Comparison operator, defaults to '='. + * @param string $column Column to compare against, defaults to 'post_date'. + * @return static + */ + public function whereDate( DateTimeInterface|int|string $date, string $compare = '=', string $column = 'post_date' ): static { + if ( ! in_array( $compare, $this->date_operators, true ) ) { + throw new InvalidArgumentException( 'Invalid date comparison operator: ' . $compare ); + } + + $this->date_constraints[] = compact( 'date', 'compare', 'column' ); + + return $this; + } + + // public function whereDateBetween( + // DateTimeInterface|int|string $start, + // DateTimeInterface|int|string $end, + // string $column = 'post_date' + // ): static { + // $this->date_constraints[] = [ + // 'date1' => $date1, + // 'date2' => $date2, + // 'column' => $column, + // ]; + + // return $this; + // } + + /** + * Query for objects older than the given date. + * + * @param DateTimeInterface|int $date + * @return static + */ + public function olderThan( DateTimeInterface|int $date ): static { + return $this->whereDate( $date, '<' ); + } + + /** + * Query for objects older than or equal to the given date. + * + * @param DateTimeInterface|int $date + * @return static + */ + public function olderThanOrEqualTo( DateTimeInterface|int $date ): static { + return $this->whereDate( $date, '<=' ); + } + + /** + * Alias for olderThan(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function older_than( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->olderThan( $date, $column ); + } + + /** + * Alias for olderThanOrEqualTo(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function older_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '<=', $column ); + } + + /** + * Query for objects newer than the given date. + * + * @param DateTimeInterface|int $date + * @param string $column Column to compare against. + * @return static + */ + public function newerThan( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '>', $column ); + } + + /** + * Query for objects newer than or equal to the given date. + * + * @param DateTimeInterface|int $date + * @param string $column Column to compare against. + * @return static + */ + public function newerThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '>=', $column ); + } + + /** + * Alias for newerThan(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function newer_than( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->newerThan( $date, $column ); + } + + /** + * Alias for newerThanOrEqualTo(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function newer_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->newerThanOrEqualTo( $date, $column ); + } + + /** + * Calculate the arguments for the date query to pass to either WP_Query. + * + * @return array + */ + protected function get_date_query_args(): array { + if ( empty( $this->date_constraints ) ) { + return []; + } + + $date_query = []; + + foreach ( $this->date_constraints as $constraint ) { + $date = $constraint['date']; + + if ( is_int( $date ) ) { + $date = Carbon::createFromTimestamp( $date, wp_timezone() ); + } elseif ( is_string( $date ) ) { + $date = Carbon::parse( $date, wp_timezone() ); + } elseif ( $date instanceof DateTimeInterface ) { + $date = Carbon::instance( $date ); + } + + switch ( $constraint['compare'] ) { + case '<': + $date_query[] = [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + ]; + break; + + case '<=': + $date_query[] = [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + 'inclusive' => true, + ]; + break; + + case '>': + $date_query[] = [ + 'column' => $constraint['column'], + 'after' => $date->toDateTimeString(), + ]; + break; + + case '>=': + $date_query[] = [ + 'column' => $constraint['column'], + 'after' => $constraint['date']->toDateTimeString(), + 'inclusive' => true, + ]; + break; + + case '=': + case '!=': + $date_query[] = [ + 'compare' => $constraint['compare'], + // $constraint['column'] => $date->toDateTimeString(), + // 'post_d' + 'column' => $constraint['column'], + 'year' => $date->format( 'Y' ), + 'month' => $date->format( 'm' ), + 'day' => $date->format( 'd' ), + 'hour' => $date->format( 'H' ), + 'minute' => $date->format( 'i' ), + 'second' => $date->format( 's' ), + ]; + break; + } + } + + return [ + 'date_query' => $date_query, + ]; + + $query = new \WP_Date_Query( $date_query ); + dd( $query->get_sql() ); + + dd( $this->date_constraints ); + } +} diff --git a/tests/database/query/test-post-query-builder.php b/tests/database/query/test-post-query-builder.php index c0fa3a35..e8cd2867 100644 --- a/tests/database/query/test-post-query-builder.php +++ b/tests/database/query/test-post-query-builder.php @@ -18,7 +18,9 @@ class Test_Post_Query_Builder extends Framework_Test_Case { protected function setUp(): void { parent::setUp(); + Utils::delete_all_data(); + register_post_type( Another_Testable_Post::get_object_name() ); } @@ -525,6 +527,60 @@ public function test_count() { $this->assertEquals( 1, Testable_Post::whereIn( 'id', [ $post_id ] )->count() ); } + public function test_date() { + $date = Carbon::now( wp_timezone() )->subMonth(); + + $expected = Testable_Post::factory()->create( [ + 'post_date' => $date->toDateTimeString(), + ] ); + + $other = Testable_Post::factory()->create( [ + 'post_date' => $date->clone()->nowWithSameTz()->toDateTimeString(), + ] ); + + // $this->assertEquals( $expected, Testable_Post::query()->dumpSql()->whereDate( $date )->first()->id ); + // $this->assertEquals( $expected, Testable_Post::query()->whereDate( $date->toDateTimeString() )->first()->id ); + // $this->assertFalse( Testable_Post::query()->whereDate( $date->addDay() )->exists() ); + $this->assertEquals( $other, Testable_Post::query()->dumpSql()->whereDate( $date->toDateTimeString(), '!=' )->first()?->id ); + } + + /** + * @dataProvider date_comparison_provider + */ + public function test_date_comparisons( int $expected, string $method, array $args ) { + $start = Carbon::now( wp_timezone() )->subMonth()->startOfDay(); + + static::factory()->post->create_ordered_set( 20, [], $start->clone() ); + + $this->assertEquals( + $expected, + Testable_Post::query()->{$method}( ...$args )->count(), + ); + } + + public static function date_comparison_provider(): array { + $start = Carbon::now( wp_timezone() )->subMonth()->startOfDay(); + + return [ + // Older than now should return all posts. + 'older_than_now' => [ 20, 'olderThan', [ Carbon::now( wp_timezone() ) ] ], + // Older than the start date should return no posts. + 'older_than_start' => [ 0, 'olderThan', [ $start ] ], + // Older than or equal to the start date should return the first post. + 'older_than_or_equal_to_start' => [ 1, 'olderThanOrEqualTo', [ $start ] ], + // Older than 5 hrs from start should return 5 posts. + 'older_than_5_hrs' => [ 5, 'olderThan', [ $start->clone()->addHours( 5 ) ] ], + // Newer than 5 hrs from start should return 14 posts. + 'newer_than_5_hrs' => [ 14, 'newerThan', [ $start->clone()->addHours( 5 ) ] ], + // Newer than or equal to 5 hrs from start should return 15 posts. + 'newer_than_or_equal_to_5_hrs' => [ 15, 'newerThanOrEqualTo', [ $start->clone()->addHours( 5 ) ] ], + // Older than the middle post should return 10 posts. + 'older_than_middle' => [ 10, 'olderThan', [ $start->clone()->addHours( 10 ) ] ], + // Newer than the middle post should return 10 posts. + 'newer_than_middle' => [ 10, 'newerThanOrEqualTo', [ $start->clone()->addHours( 10 ) ] ], + ]; + } + /** * Get a random post ID, ensures the post ID is not the last in the set. * From c91d15ae67db0524d15946e272b233dd827b04f1 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 14 Sep 2023 15:30:21 -0400 Subject: [PATCH 05/32] Adding in the basis of the date query --- .../query/concerns/trait-queries-dates.php | 116 ++++++++++++------ .../concerns/trait-wordpress-state.php | 23 ++-- .../query/test-post-query-builder.php | 67 ++++++++-- 3 files changed, 149 insertions(+), 57 deletions(-) diff --git a/src/mantle/database/query/concerns/trait-queries-dates.php b/src/mantle/database/query/concerns/trait-queries-dates.php index ff5e7afd..3fb96588 100644 --- a/src/mantle/database/query/concerns/trait-queries-dates.php +++ b/src/mantle/database/query/concerns/trait-queries-dates.php @@ -2,6 +2,8 @@ /** * Queries_Dates trait file * + * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid + * * @package Mantle */ @@ -22,7 +24,7 @@ trait Queries_Dates { /** * Date constraints to apply to the query. * - * @var array> + * @var array */ protected array $date_constraints = []; @@ -41,11 +43,15 @@ trait Queries_Dates { ]; /** - * Add a date query for a specific date to the query. + * Add a date query for a date to the query. + * + * Defaults to comparing against the post published date. + * + * @throws InvalidArgumentException If an invalid comparison operator is provided. * * @param DateTimeInterface|int|string $date - * @param string $compare Comparison operator, defaults to '='. - * @param string $column Column to compare against, defaults to 'post_date'. + * @param string $compare Comparison operator, defaults to '='. + * @param string $column Column to compare against, defaults to 'post_date'. * @return static */ public function whereDate( DateTimeInterface|int|string $date, string $compare = '=', string $column = 'post_date' ): static { @@ -58,19 +64,38 @@ public function whereDate( DateTimeInterface|int|string $date, string $compare = return $this; } - // public function whereDateBetween( - // DateTimeInterface|int|string $start, - // DateTimeInterface|int|string $end, - // string $column = 'post_date' - // ): static { - // $this->date_constraints[] = [ - // 'date1' => $date1, - // 'date2' => $date2, - // 'column' => $column, - // ]; + /** + * Add a date query for the UTC publish date to the query. + * + * @param DateTimeInterface|int|string $date Date to compare against. + * @param string $compare Comparison operator, defaults to '='. + * @return static + */ + public function whereUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ): static { + return $this->whereDate( $date, $compare, 'post_date_gmt' ); + } + + /** + * Add a date query for the modified date to the query. + * + * @param DateTimeInterface|int|string $date Date to compare against. + * @param string $compare Comparison operator, defaults to '='. + * @return static + */ + public function whereModifiedDate( DateTimeInterface|int|string $date, string $compare = '=' ): static { + return $this->whereDate( $date, $compare, 'post_modified' ); + } - // return $this; - // } + /** + * Add a date query for the modified UTC date to the query. + * + * @param DateTimeInterface|int|string $date Date to compare against. + * @param string $compare Comparison operator, defaults to '='. + * @return static + */ + public function whereModifiedUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ): static { + return $this->whereDate( $date, $compare, 'post_modified_gmt' ); + } /** * Query for objects older than the given date. @@ -96,18 +121,17 @@ public function olderThanOrEqualTo( DateTimeInterface|int $date ): static { * Alias for olderThan(). * * @param DateTimeInterface|int $date Date to compare against. - * @param string $column Column to compare against. * @return static */ - public function older_than( DateTimeInterface|int $date, string $column = 'post_date' ): static { - return $this->olderThan( $date, $column ); + public function older_than( DateTimeInterface|int $date ): static { + return $this->olderThan( $date ); } /** * Alias for olderThanOrEqualTo(). * * @param DateTimeInterface|int $date Date to compare against. - * @param string $column Column to compare against. + * @param string $column Column to compare against. * @return static */ public function older_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ): static { @@ -118,7 +142,7 @@ public function older_than_or_equal_to( DateTimeInterface|int $date, string $col * Query for objects newer than the given date. * * @param DateTimeInterface|int $date - * @param string $column Column to compare against. + * @param string $column Column to compare against. * @return static */ public function newerThan( DateTimeInterface|int $date, string $column = 'post_date' ): static { @@ -129,7 +153,7 @@ public function newerThan( DateTimeInterface|int $date, string $column = 'post_d * Query for objects newer than or equal to the given date. * * @param DateTimeInterface|int $date - * @param string $column Column to compare against. + * @param string $column Column to compare against. * @return static */ public function newerThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ): static { @@ -140,7 +164,7 @@ public function newerThanOrEqualTo( DateTimeInterface|int $date, string $column * Alias for newerThan(). * * @param DateTimeInterface|int $date Date to compare against. - * @param string $column Column to compare against. + * @param string $column Column to compare against. * @return static */ public function newer_than( DateTimeInterface|int $date, string $column = 'post_date' ): static { @@ -151,7 +175,7 @@ public function newer_than( DateTimeInterface|int $date, string $column = 'post_ * Alias for newerThanOrEqualTo(). * * @param DateTimeInterface|int $date Date to compare against. - * @param string $column Column to compare against. + * @param string $column Column to compare against. * @return static */ public function newer_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ): static { @@ -207,24 +231,41 @@ protected function get_date_query_args(): array { case '>=': $date_query[] = [ 'column' => $constraint['column'], - 'after' => $constraint['date']->toDateTimeString(), + 'after' => $date->toDateTimeString(), 'inclusive' => true, ]; break; + // TODO: Review if a query for a specific date can be improved. case '=': + $date_query[] = [ + 'relation' => 'and', + [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + 'inclusive' => true, + ], + [ + 'column' => $constraint['column'], + 'after' => $date->toDateTimeString(), + 'inclusive' => true, + ], + ]; + break; + case '!=': $date_query[] = [ - 'compare' => $constraint['compare'], - // $constraint['column'] => $date->toDateTimeString(), - // 'post_d' - 'column' => $constraint['column'], - 'year' => $date->format( 'Y' ), - 'month' => $date->format( 'm' ), - 'day' => $date->format( 'd' ), - 'hour' => $date->format( 'H' ), - 'minute' => $date->format( 'i' ), - 'second' => $date->format( 's' ), + 'relation' => 'or', + [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + 'inclusive' => false, + ], + [ + 'column' => $constraint['column'], + 'after' => $date->toDateTimeString(), + 'inclusive' => false, + ], ]; break; } @@ -233,10 +274,5 @@ protected function get_date_query_args(): array { return [ 'date_query' => $date_query, ]; - - $query = new \WP_Date_Query( $date_query ); - dd( $query->get_sql() ); - - dd( $this->date_constraints ); } } diff --git a/src/mantle/testing/concerns/trait-wordpress-state.php b/src/mantle/testing/concerns/trait-wordpress-state.php index bdb34666..4b38229a 100644 --- a/src/mantle/testing/concerns/trait-wordpress-state.php +++ b/src/mantle/testing/concerns/trait-wordpress-state.php @@ -7,7 +7,11 @@ namespace Mantle\Testing\Concerns; +use Carbon\Carbon; +use DateTimeInterface; +use Mantle\Database\Model\Post; use Mantle\Testing\Utils; +use WP_Post; /** * This trait includes functionality for controlling WordPress state during @@ -159,22 +163,27 @@ public function set_permalink_structure( $structure = '' ) { * * @global \wpdb $wpdb WordPress database abstraction object. * - * @param int $post_id Post ID. - * @param string $date Post date, in the format YYYY-MM-DD HH:MM:SS. + * @param WP_Post|Post|int $post Post ID or post object. + * @param DateTimeInterface|string $date Date object or string to update the + * post with. If a string is passed it + * is assumed to be local timezone. * @return int|false 1 on success, or false on error. */ - protected function update_post_modified( $post_id, $date ) { + protected function update_post_modified( WP_Post|Post|int $post, DateTimeInterface|string $date ) { global $wpdb; + $post = is_object( $post ) ? $post->ID : $post; + $date = $date instanceof DateTimeInterface ? Carbon::instance( $date ) : Carbon::parse( $date, wp_timezone() ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery $update = $wpdb->update( $wpdb->posts, [ - 'post_modified' => $date, - 'post_modified_gmt' => $date, + 'post_modified' => $date->setTimezone( wp_timezone() )->format( 'Y-m-d H:i:s' ), + 'post_modified_gmt' => $date->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' ), ], [ - 'ID' => $post_id, + 'ID' => $post, ], [ '%s', @@ -185,7 +194,7 @@ protected function update_post_modified( $post_id, $date ) { ] ); - clean_post_cache( $post_id ); + clean_post_cache( $post ); return $update; } diff --git a/tests/database/query/test-post-query-builder.php b/tests/database/query/test-post-query-builder.php index e8cd2867..bbfb2ada 100644 --- a/tests/database/query/test-post-query-builder.php +++ b/tests/database/query/test-post-query-builder.php @@ -527,21 +527,68 @@ public function test_count() { $this->assertEquals( 1, Testable_Post::whereIn( 'id', [ $post_id ] )->count() ); } - public function test_date() { - $date = Carbon::now( wp_timezone() )->subMonth(); + public function test_post_by_date() { + $old_date = Carbon::now( wp_timezone() )->subMonth(); + $now = Carbon::now( wp_timezone() ); - $expected = Testable_Post::factory()->create( [ - 'post_date' => $date->toDateTimeString(), + $old_post_id = Testable_Post::factory()->create( [ + 'post_date' => $old_date->toDateTimeString(), ] ); - $other = Testable_Post::factory()->create( [ - 'post_date' => $date->clone()->nowWithSameTz()->toDateTimeString(), + $now_post_id = Testable_Post::factory()->create( [ + 'post_date' => $now->toDateTimeString(), ] ); - // $this->assertEquals( $expected, Testable_Post::query()->dumpSql()->whereDate( $date )->first()->id ); - // $this->assertEquals( $expected, Testable_Post::query()->whereDate( $date->toDateTimeString() )->first()->id ); - // $this->assertFalse( Testable_Post::query()->whereDate( $date->addDay() )->exists() ); - $this->assertEquals( $other, Testable_Post::query()->dumpSql()->whereDate( $date->toDateTimeString(), '!=' )->first()?->id ); + $this->assertEquals( + $old_post_id, + Testable_Post::query()->whereDate( $old_date )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereDate( $now )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereDate( $old_date, '!=' )->first()?->id, + ); + + $this->assertEquals( + $old_post_id, + Testable_Post::query()->whereDate( $now, '!=' )->first()?->id, + ); + } + + public function test_post_by_modified_date() { + $old_date = Carbon::now( wp_timezone() )->subMonth(); + $now = Carbon::now( wp_timezone() ); + + $old_post_id = Testable_Post::factory()->create(); + $now_post_id = Testable_Post::factory()->create(); + + $this->update_post_modified( $old_post_id, $old_date ); + $this->update_post_modified( $now_post_id, $now ); + + $this->assertEquals( + $old_post_id, + Testable_Post::query()->whereModifiedDate( $old_date )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereModifiedDate( $now )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereModifiedDate( $old_date, '!=' )->first()?->id, + ); + + $this->assertEquals( + $old_post_id, + Testable_Post::query()->whereModifiedDate( $now, '!=' )->first()?->id, + ); } /** From f5383db6aac94ac7d2477df08378b0ca54695e32 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 14 Sep 2023 15:45:04 -0400 Subject: [PATCH 06/32] Ensure where() can be used still --- src/mantle/database/query/class-builder.php | 5 +++++ src/mantle/database/query/class-post-query-builder.php | 6 ++++++ tests/database/query/test-post-query-builder.php | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/src/mantle/database/query/class-builder.php b/src/mantle/database/query/class-builder.php index 401cf85c..da6c1779 100644 --- a/src/mantle/database/query/class-builder.php +++ b/src/mantle/database/query/class-builder.php @@ -302,6 +302,11 @@ public function where( array|string $attribute, mixed $value = '' ): static { $attribute = $this->resolve_attribute( $attribute ); + // Pass date attributes to the date query builder if available. + if ( method_exists( $this, 'whereDate' ) && in_array( $attribute, [ 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt' ], true ) ) { + return $this->whereDate( $value, '=', $attribute ); + } + $this->wheres[ $attribute ] = $value; return $this; diff --git a/src/mantle/database/query/class-post-query-builder.php b/src/mantle/database/query/class-post-query-builder.php index 74e6dfc2..ca88e7b6 100644 --- a/src/mantle/database/query/class-post-query-builder.php +++ b/src/mantle/database/query/class-post-query-builder.php @@ -39,7 +39,13 @@ class Post_Query_Builder extends Builder { * @var array */ protected array $query_aliases = [ + 'date_gmt' => 'post_date_gmt', + 'date_utc' => 'post_date_gmt', + 'date' => 'post_date', 'id' => 'p', + 'modified_gmt' => 'post_modified_gmt', + 'modified_utc' => 'post_modified_gmt', + 'modified' => 'post_modified', 'post_author' => 'author', 'post_name' => 'name', 'slug' => 'name', diff --git a/tests/database/query/test-post-query-builder.php b/tests/database/query/test-post-query-builder.php index bbfb2ada..c735ad73 100644 --- a/tests/database/query/test-post-query-builder.php +++ b/tests/database/query/test-post-query-builder.php @@ -549,6 +549,11 @@ public function test_post_by_date() { Testable_Post::query()->whereDate( $now )->first()?->id, ); + $this->assertEquals( + $old_post_id, + Testable_Post::query()->where( 'date', $old_date )->first()?->id, + ); + $this->assertEquals( $now_post_id, Testable_Post::query()->whereDate( $old_date, '!=' )->first()?->id, From a5426bed5d3465c9c5babb2e79f3ece7063a40b7 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 14 Sep 2023 15:50:26 -0400 Subject: [PATCH 07/32] TODO --- src/mantle/database/query/concerns/trait-queries-dates.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mantle/database/query/concerns/trait-queries-dates.php b/src/mantle/database/query/concerns/trait-queries-dates.php index 3fb96588..fbe6c9a6 100644 --- a/src/mantle/database/query/concerns/trait-queries-dates.php +++ b/src/mantle/database/query/concerns/trait-queries-dates.php @@ -18,6 +18,8 @@ * * @link https://developer.wordpress.org/reference/classes/wp_query/#date-parameters * + * @todo Add support for more complex date queries (mixing AND/OR, etc.). + * * @mixin \Mantle\Database\Query\Post_Query_Builder */ trait Queries_Dates { From d00a643d7a1b41165e7b75b680f67848d72e84a1 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 14 Sep 2023 15:53:56 -0400 Subject: [PATCH 08/32] PHPCS fix --- .../database/query/class-post-query-builder.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mantle/database/query/class-post-query-builder.php b/src/mantle/database/query/class-post-query-builder.php index ca88e7b6..c5ad15ae 100644 --- a/src/mantle/database/query/class-post-query-builder.php +++ b/src/mantle/database/query/class-post-query-builder.php @@ -39,16 +39,16 @@ class Post_Query_Builder extends Builder { * @var array */ protected array $query_aliases = [ - 'date_gmt' => 'post_date_gmt', - 'date_utc' => 'post_date_gmt', - 'date' => 'post_date', - 'id' => 'p', + 'date_gmt' => 'post_date_gmt', + 'date_utc' => 'post_date_gmt', + 'date' => 'post_date', + 'id' => 'p', 'modified_gmt' => 'post_modified_gmt', 'modified_utc' => 'post_modified_gmt', - 'modified' => 'post_modified', - 'post_author' => 'author', - 'post_name' => 'name', - 'slug' => 'name', + 'modified' => 'post_modified', + 'post_author' => 'author', + 'post_name' => 'name', + 'slug' => 'name', ]; /** From 151dd5881ec8120f50317e11a9854a0b4a27da24 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Thu, 14 Sep 2023 15:57:44 -0400 Subject: [PATCH 09/32] Adding the doc blocks for the date query --- src/mantle/database/model/class-post.php | 12 ++++++++++++ tests/database/query/test-post-query-builder.php | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/mantle/database/model/class-post.php b/src/mantle/database/model/class-post.php index 65eb4157..dd9773a9 100644 --- a/src/mantle/database/model/class-post.php +++ b/src/mantle/database/model/class-post.php @@ -67,6 +67,18 @@ * @method static \Mantle\Database\Query\Post_Query_Builder where_raw( array|string $column, ?string $operator = null, mixed $value = null, string $boolean = 'AND' ) * @method static \Mantle\Database\Query\Post_Query_Builder orWhereRaw( array|string $column, ?string $operator = null, mixed $value = null, string $boolean = 'AND' ) * @method static \Mantle\Database\Query\Post_Query_Builder or_where_raw( array|string $column, ?string $operator = null, mixed $value = null, string $boolean = 'AND' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereDate( DateTimeInterface|int|string $date, string $compare = '=', string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereModifiedDate( DateTimeInterface|int|string $date, string $compare = '=' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereModifiedUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ) + * @method static \Mantle\Database\Query\Post_Query_Builder olderThan( DateTimeInterface|int $date ) + * @method static \Mantle\Database\Query\Post_Query_Builder olderThanOrEqualTo( DateTimeInterface|int $date ) + * @method static \Mantle\Database\Query\Post_Query_Builder older_than( DateTimeInterface|int $date ) + * @method static \Mantle\Database\Query\Post_Query_Builder older_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newerThan( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newerThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newer_than( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newer_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ) */ class Post extends Model implements Contracts\Database\Core_Object, Contracts\Database\Model_Meta, Contracts\Database\Updatable { use Events\Post_Events, diff --git a/tests/database/query/test-post-query-builder.php b/tests/database/query/test-post-query-builder.php index c735ad73..73c0ba32 100644 --- a/tests/database/query/test-post-query-builder.php +++ b/tests/database/query/test-post-query-builder.php @@ -541,7 +541,7 @@ public function test_post_by_date() { $this->assertEquals( $old_post_id, - Testable_Post::query()->whereDate( $old_date )->first()?->id, + Testable_Post::whereDate( $old_date )->first()?->id, ); $this->assertEquals( From cab1b3ada306a08167a6b6cb1975dc255bf4d4ee Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Wed, 20 Sep 2023 16:39:33 -0400 Subject: [PATCH 10/32] Ensure all methods have parity and can pass the column --- src/mantle/application/autoload.php | 18 +++++++++ .../query/concerns/trait-queries-dates.php | 39 +++++++++++++++---- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/mantle/application/autoload.php b/src/mantle/application/autoload.php index 34420cc2..e7d49d7d 100644 --- a/src/mantle/application/autoload.php +++ b/src/mantle/application/autoload.php @@ -82,3 +82,21 @@ function storage_path( string $path = '' ): string { return app()->get_storage_path( $path ); } } + +if ( ! function_exists( 'now' ) ) { + /** + * Create a new Carbon instance for the current time. + * + * @todo Allow this to be faked and mocked during testing. + * + * @param DateTimeZone|string|null $tz Timezone. + * @return Carbon\Carbon + */ + function now( \DateTimeZone|string|null $tz = null ): Carbon\Carbon { + if ( ! $tz ) { + $tz = function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( 'UTC' ); + } + + return Carbon\Carbon::now( $tz ); + } +} diff --git a/src/mantle/database/query/concerns/trait-queries-dates.php b/src/mantle/database/query/concerns/trait-queries-dates.php index fbe6c9a6..7e269e1a 100644 --- a/src/mantle/database/query/concerns/trait-queries-dates.php +++ b/src/mantle/database/query/concerns/trait-queries-dates.php @@ -102,31 +102,44 @@ public function whereModifiedUtcDate( DateTimeInterface|int|string $date, string /** * Query for objects older than the given date. * - * @param DateTimeInterface|int $date + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. * @return static */ - public function olderThan( DateTimeInterface|int $date ): static { - return $this->whereDate( $date, '<' ); + public function olderThan( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '<', $column ); } /** * Query for objects older than or equal to the given date. * - * @param DateTimeInterface|int $date + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function olderThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '<=', $column ); + } + + /** + * Query for objects older than or equal to now. + * + * @param string $column Column to compare against. * @return static */ - public function olderThanOrEqualTo( DateTimeInterface|int $date ): static { - return $this->whereDate( $date, '<=' ); + public function olderThanNow( string $column = 'post_date' ): static { + return $this->olderThanOrEqualTo( now(), $column ); } /** * Alias for olderThan(). * * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. * @return static */ - public function older_than( DateTimeInterface|int $date ): static { - return $this->olderThan( $date ); + public function older_than( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->olderThan( $date, $column ); } /** @@ -151,6 +164,16 @@ public function newerThan( DateTimeInterface|int $date, string $column = 'post_d return $this->whereDate( $date, '>', $column ); } + /** + * Query for objects newer than now (in the future from now). + * + * @param string $column Column to compare against. + * @return static + */ + public function newerThanNow( string $column = 'post_date' ): static { + return $this->newerThan( now(), $column ); + } + /** * Query for objects newer than or equal to the given date. * From bcfab3efe1a8490a1b2a75307a4989a082cbbd74 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Fri, 22 Sep 2023 15:30:29 -0400 Subject: [PATCH 11/32] Fixing @methods --- src/mantle/database/model/class-post.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mantle/database/model/class-post.php b/src/mantle/database/model/class-post.php index dd9773a9..fc3c7021 100644 --- a/src/mantle/database/model/class-post.php +++ b/src/mantle/database/model/class-post.php @@ -71,9 +71,9 @@ * @method static \Mantle\Database\Query\Post_Query_Builder whereUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ) * @method static \Mantle\Database\Query\Post_Query_Builder whereModifiedDate( DateTimeInterface|int|string $date, string $compare = '=' ) * @method static \Mantle\Database\Query\Post_Query_Builder whereModifiedUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ) - * @method static \Mantle\Database\Query\Post_Query_Builder olderThan( DateTimeInterface|int $date ) - * @method static \Mantle\Database\Query\Post_Query_Builder olderThanOrEqualTo( DateTimeInterface|int $date ) - * @method static \Mantle\Database\Query\Post_Query_Builder older_than( DateTimeInterface|int $date ) + * @method static \Mantle\Database\Query\Post_Query_Builder olderThan( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder olderThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder older_than( DateTimeInterface|int $date, string $column = 'post_date' ) * @method static \Mantle\Database\Query\Post_Query_Builder older_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ) * @method static \Mantle\Database\Query\Post_Query_Builder newerThan( DateTimeInterface|int $date, string $column = 'post_date' ) * @method static \Mantle\Database\Query\Post_Query_Builder newerThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ) From 7075e2e34a54eef11680d08617196851bfb54005 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Fri, 22 Sep 2023 15:35:11 -0400 Subject: [PATCH 12/32] Adding date query builder for posts (#454) * WIP * Adding in the basis of the date query * Ensure where() can be used still * TODO * PHPCS fix * Adding the doc blocks for the date query * Ensure all methods have parity and can pass the column * Fixing @methods --- src/mantle/application/autoload.php | 18 ++ src/mantle/database/model/class-post.php | 12 + src/mantle/database/query/class-builder.php | 5 + .../query/class-post-query-builder.php | 18 +- .../query/concerns/trait-queries-dates.php | 303 ++++++++++++++++++ .../concerns/trait-wordpress-state.php | 23 +- .../query/test-post-query-builder.php | 108 +++++++ 7 files changed, 475 insertions(+), 12 deletions(-) create mode 100644 src/mantle/database/query/concerns/trait-queries-dates.php diff --git a/src/mantle/application/autoload.php b/src/mantle/application/autoload.php index 34420cc2..e7d49d7d 100644 --- a/src/mantle/application/autoload.php +++ b/src/mantle/application/autoload.php @@ -82,3 +82,21 @@ function storage_path( string $path = '' ): string { return app()->get_storage_path( $path ); } } + +if ( ! function_exists( 'now' ) ) { + /** + * Create a new Carbon instance for the current time. + * + * @todo Allow this to be faked and mocked during testing. + * + * @param DateTimeZone|string|null $tz Timezone. + * @return Carbon\Carbon + */ + function now( \DateTimeZone|string|null $tz = null ): Carbon\Carbon { + if ( ! $tz ) { + $tz = function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( 'UTC' ); + } + + return Carbon\Carbon::now( $tz ); + } +} diff --git a/src/mantle/database/model/class-post.php b/src/mantle/database/model/class-post.php index 65eb4157..fc3c7021 100644 --- a/src/mantle/database/model/class-post.php +++ b/src/mantle/database/model/class-post.php @@ -67,6 +67,18 @@ * @method static \Mantle\Database\Query\Post_Query_Builder where_raw( array|string $column, ?string $operator = null, mixed $value = null, string $boolean = 'AND' ) * @method static \Mantle\Database\Query\Post_Query_Builder orWhereRaw( array|string $column, ?string $operator = null, mixed $value = null, string $boolean = 'AND' ) * @method static \Mantle\Database\Query\Post_Query_Builder or_where_raw( array|string $column, ?string $operator = null, mixed $value = null, string $boolean = 'AND' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereDate( DateTimeInterface|int|string $date, string $compare = '=', string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereModifiedDate( DateTimeInterface|int|string $date, string $compare = '=' ) + * @method static \Mantle\Database\Query\Post_Query_Builder whereModifiedUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ) + * @method static \Mantle\Database\Query\Post_Query_Builder olderThan( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder olderThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder older_than( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder older_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newerThan( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newerThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newer_than( DateTimeInterface|int $date, string $column = 'post_date' ) + * @method static \Mantle\Database\Query\Post_Query_Builder newer_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ) */ class Post extends Model implements Contracts\Database\Core_Object, Contracts\Database\Model_Meta, Contracts\Database\Updatable { use Events\Post_Events, diff --git a/src/mantle/database/query/class-builder.php b/src/mantle/database/query/class-builder.php index 401cf85c..da6c1779 100644 --- a/src/mantle/database/query/class-builder.php +++ b/src/mantle/database/query/class-builder.php @@ -302,6 +302,11 @@ public function where( array|string $attribute, mixed $value = '' ): static { $attribute = $this->resolve_attribute( $attribute ); + // Pass date attributes to the date query builder if available. + if ( method_exists( $this, 'whereDate' ) && in_array( $attribute, [ 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt' ], true ) ) { + return $this->whereDate( $value, '=', $attribute ); + } + $this->wheres[ $attribute ] = $value; return $this; diff --git a/src/mantle/database/query/class-post-query-builder.php b/src/mantle/database/query/class-post-query-builder.php index 08f2158f..c5ad15ae 100644 --- a/src/mantle/database/query/class-post-query-builder.php +++ b/src/mantle/database/query/class-post-query-builder.php @@ -9,6 +9,7 @@ namespace Mantle\Database\Query; use Mantle\Database\Model\Term; +use Mantle\Database\Query\Concerns\Queries_Dates; use Mantle\Support\Helpers; use Mantle\Support\Collection; use WP_Term; @@ -30,7 +31,7 @@ * @method \Mantle\Database\Query\Post_Query_Builder whereType( string $type ) */ class Post_Query_Builder extends Builder { - use Queries_Relationships; + use Queries_Dates, Queries_Relationships; /** * Query Variable Aliases @@ -38,10 +39,16 @@ class Post_Query_Builder extends Builder { * @var array */ protected array $query_aliases = [ - 'id' => 'p', - 'post_author' => 'author', - 'post_name' => 'name', - 'slug' => 'name', + 'date_gmt' => 'post_date_gmt', + 'date_utc' => 'post_date_gmt', + 'date' => 'post_date', + 'id' => 'p', + 'modified_gmt' => 'post_modified_gmt', + 'modified_utc' => 'post_modified_gmt', + 'modified' => 'post_modified', + 'post_author' => 'author', + 'post_name' => 'name', + 'slug' => 'name', ]; /** @@ -121,6 +128,7 @@ public function get_query_args(): array { 'suppress_filters' => false, 'tax_query' => $this->tax_query, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query ], + $this->get_date_query_args(), $this->wheres, ); } diff --git a/src/mantle/database/query/concerns/trait-queries-dates.php b/src/mantle/database/query/concerns/trait-queries-dates.php new file mode 100644 index 00000000..7e269e1a --- /dev/null +++ b/src/mantle/database/query/concerns/trait-queries-dates.php @@ -0,0 +1,303 @@ + + */ + protected array $date_constraints = []; + + /** + * The valid comparison operators for a date query. + * + * @var array + */ + protected array $date_operators = [ + '=', + '!=', + '>', + '>=', + '<', + '<=', + ]; + + /** + * Add a date query for a date to the query. + * + * Defaults to comparing against the post published date. + * + * @throws InvalidArgumentException If an invalid comparison operator is provided. + * + * @param DateTimeInterface|int|string $date + * @param string $compare Comparison operator, defaults to '='. + * @param string $column Column to compare against, defaults to 'post_date'. + * @return static + */ + public function whereDate( DateTimeInterface|int|string $date, string $compare = '=', string $column = 'post_date' ): static { + if ( ! in_array( $compare, $this->date_operators, true ) ) { + throw new InvalidArgumentException( 'Invalid date comparison operator: ' . $compare ); + } + + $this->date_constraints[] = compact( 'date', 'compare', 'column' ); + + return $this; + } + + /** + * Add a date query for the UTC publish date to the query. + * + * @param DateTimeInterface|int|string $date Date to compare against. + * @param string $compare Comparison operator, defaults to '='. + * @return static + */ + public function whereUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ): static { + return $this->whereDate( $date, $compare, 'post_date_gmt' ); + } + + /** + * Add a date query for the modified date to the query. + * + * @param DateTimeInterface|int|string $date Date to compare against. + * @param string $compare Comparison operator, defaults to '='. + * @return static + */ + public function whereModifiedDate( DateTimeInterface|int|string $date, string $compare = '=' ): static { + return $this->whereDate( $date, $compare, 'post_modified' ); + } + + /** + * Add a date query for the modified UTC date to the query. + * + * @param DateTimeInterface|int|string $date Date to compare against. + * @param string $compare Comparison operator, defaults to '='. + * @return static + */ + public function whereModifiedUtcDate( DateTimeInterface|int|string $date, string $compare = '=' ): static { + return $this->whereDate( $date, $compare, 'post_modified_gmt' ); + } + + /** + * Query for objects older than the given date. + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function olderThan( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '<', $column ); + } + + /** + * Query for objects older than or equal to the given date. + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function olderThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '<=', $column ); + } + + /** + * Query for objects older than or equal to now. + * + * @param string $column Column to compare against. + * @return static + */ + public function olderThanNow( string $column = 'post_date' ): static { + return $this->olderThanOrEqualTo( now(), $column ); + } + + /** + * Alias for olderThan(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function older_than( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->olderThan( $date, $column ); + } + + /** + * Alias for olderThanOrEqualTo(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function older_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '<=', $column ); + } + + /** + * Query for objects newer than the given date. + * + * @param DateTimeInterface|int $date + * @param string $column Column to compare against. + * @return static + */ + public function newerThan( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '>', $column ); + } + + /** + * Query for objects newer than now (in the future from now). + * + * @param string $column Column to compare against. + * @return static + */ + public function newerThanNow( string $column = 'post_date' ): static { + return $this->newerThan( now(), $column ); + } + + /** + * Query for objects newer than or equal to the given date. + * + * @param DateTimeInterface|int $date + * @param string $column Column to compare against. + * @return static + */ + public function newerThanOrEqualTo( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->whereDate( $date, '>=', $column ); + } + + /** + * Alias for newerThan(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function newer_than( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->newerThan( $date, $column ); + } + + /** + * Alias for newerThanOrEqualTo(). + * + * @param DateTimeInterface|int $date Date to compare against. + * @param string $column Column to compare against. + * @return static + */ + public function newer_than_or_equal_to( DateTimeInterface|int $date, string $column = 'post_date' ): static { + return $this->newerThanOrEqualTo( $date, $column ); + } + + /** + * Calculate the arguments for the date query to pass to either WP_Query. + * + * @return array + */ + protected function get_date_query_args(): array { + if ( empty( $this->date_constraints ) ) { + return []; + } + + $date_query = []; + + foreach ( $this->date_constraints as $constraint ) { + $date = $constraint['date']; + + if ( is_int( $date ) ) { + $date = Carbon::createFromTimestamp( $date, wp_timezone() ); + } elseif ( is_string( $date ) ) { + $date = Carbon::parse( $date, wp_timezone() ); + } elseif ( $date instanceof DateTimeInterface ) { + $date = Carbon::instance( $date ); + } + + switch ( $constraint['compare'] ) { + case '<': + $date_query[] = [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + ]; + break; + + case '<=': + $date_query[] = [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + 'inclusive' => true, + ]; + break; + + case '>': + $date_query[] = [ + 'column' => $constraint['column'], + 'after' => $date->toDateTimeString(), + ]; + break; + + case '>=': + $date_query[] = [ + 'column' => $constraint['column'], + 'after' => $date->toDateTimeString(), + 'inclusive' => true, + ]; + break; + + // TODO: Review if a query for a specific date can be improved. + case '=': + $date_query[] = [ + 'relation' => 'and', + [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + 'inclusive' => true, + ], + [ + 'column' => $constraint['column'], + 'after' => $date->toDateTimeString(), + 'inclusive' => true, + ], + ]; + break; + + case '!=': + $date_query[] = [ + 'relation' => 'or', + [ + 'column' => $constraint['column'], + 'before' => $date->toDateTimeString(), + 'inclusive' => false, + ], + [ + 'column' => $constraint['column'], + 'after' => $date->toDateTimeString(), + 'inclusive' => false, + ], + ]; + break; + } + } + + return [ + 'date_query' => $date_query, + ]; + } +} diff --git a/src/mantle/testing/concerns/trait-wordpress-state.php b/src/mantle/testing/concerns/trait-wordpress-state.php index bdb34666..4b38229a 100644 --- a/src/mantle/testing/concerns/trait-wordpress-state.php +++ b/src/mantle/testing/concerns/trait-wordpress-state.php @@ -7,7 +7,11 @@ namespace Mantle\Testing\Concerns; +use Carbon\Carbon; +use DateTimeInterface; +use Mantle\Database\Model\Post; use Mantle\Testing\Utils; +use WP_Post; /** * This trait includes functionality for controlling WordPress state during @@ -159,22 +163,27 @@ public function set_permalink_structure( $structure = '' ) { * * @global \wpdb $wpdb WordPress database abstraction object. * - * @param int $post_id Post ID. - * @param string $date Post date, in the format YYYY-MM-DD HH:MM:SS. + * @param WP_Post|Post|int $post Post ID or post object. + * @param DateTimeInterface|string $date Date object or string to update the + * post with. If a string is passed it + * is assumed to be local timezone. * @return int|false 1 on success, or false on error. */ - protected function update_post_modified( $post_id, $date ) { + protected function update_post_modified( WP_Post|Post|int $post, DateTimeInterface|string $date ) { global $wpdb; + $post = is_object( $post ) ? $post->ID : $post; + $date = $date instanceof DateTimeInterface ? Carbon::instance( $date ) : Carbon::parse( $date, wp_timezone() ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery $update = $wpdb->update( $wpdb->posts, [ - 'post_modified' => $date, - 'post_modified_gmt' => $date, + 'post_modified' => $date->setTimezone( wp_timezone() )->format( 'Y-m-d H:i:s' ), + 'post_modified_gmt' => $date->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' ), ], [ - 'ID' => $post_id, + 'ID' => $post, ], [ '%s', @@ -185,7 +194,7 @@ protected function update_post_modified( $post_id, $date ) { ] ); - clean_post_cache( $post_id ); + clean_post_cache( $post ); return $update; } diff --git a/tests/database/query/test-post-query-builder.php b/tests/database/query/test-post-query-builder.php index c0fa3a35..73c0ba32 100644 --- a/tests/database/query/test-post-query-builder.php +++ b/tests/database/query/test-post-query-builder.php @@ -18,7 +18,9 @@ class Test_Post_Query_Builder extends Framework_Test_Case { protected function setUp(): void { parent::setUp(); + Utils::delete_all_data(); + register_post_type( Another_Testable_Post::get_object_name() ); } @@ -525,6 +527,112 @@ public function test_count() { $this->assertEquals( 1, Testable_Post::whereIn( 'id', [ $post_id ] )->count() ); } + public function test_post_by_date() { + $old_date = Carbon::now( wp_timezone() )->subMonth(); + $now = Carbon::now( wp_timezone() ); + + $old_post_id = Testable_Post::factory()->create( [ + 'post_date' => $old_date->toDateTimeString(), + ] ); + + $now_post_id = Testable_Post::factory()->create( [ + 'post_date' => $now->toDateTimeString(), + ] ); + + $this->assertEquals( + $old_post_id, + Testable_Post::whereDate( $old_date )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereDate( $now )->first()?->id, + ); + + $this->assertEquals( + $old_post_id, + Testable_Post::query()->where( 'date', $old_date )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereDate( $old_date, '!=' )->first()?->id, + ); + + $this->assertEquals( + $old_post_id, + Testable_Post::query()->whereDate( $now, '!=' )->first()?->id, + ); + } + + public function test_post_by_modified_date() { + $old_date = Carbon::now( wp_timezone() )->subMonth(); + $now = Carbon::now( wp_timezone() ); + + $old_post_id = Testable_Post::factory()->create(); + $now_post_id = Testable_Post::factory()->create(); + + $this->update_post_modified( $old_post_id, $old_date ); + $this->update_post_modified( $now_post_id, $now ); + + $this->assertEquals( + $old_post_id, + Testable_Post::query()->whereModifiedDate( $old_date )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereModifiedDate( $now )->first()?->id, + ); + + $this->assertEquals( + $now_post_id, + Testable_Post::query()->whereModifiedDate( $old_date, '!=' )->first()?->id, + ); + + $this->assertEquals( + $old_post_id, + Testable_Post::query()->whereModifiedDate( $now, '!=' )->first()?->id, + ); + } + + /** + * @dataProvider date_comparison_provider + */ + public function test_date_comparisons( int $expected, string $method, array $args ) { + $start = Carbon::now( wp_timezone() )->subMonth()->startOfDay(); + + static::factory()->post->create_ordered_set( 20, [], $start->clone() ); + + $this->assertEquals( + $expected, + Testable_Post::query()->{$method}( ...$args )->count(), + ); + } + + public static function date_comparison_provider(): array { + $start = Carbon::now( wp_timezone() )->subMonth()->startOfDay(); + + return [ + // Older than now should return all posts. + 'older_than_now' => [ 20, 'olderThan', [ Carbon::now( wp_timezone() ) ] ], + // Older than the start date should return no posts. + 'older_than_start' => [ 0, 'olderThan', [ $start ] ], + // Older than or equal to the start date should return the first post. + 'older_than_or_equal_to_start' => [ 1, 'olderThanOrEqualTo', [ $start ] ], + // Older than 5 hrs from start should return 5 posts. + 'older_than_5_hrs' => [ 5, 'olderThan', [ $start->clone()->addHours( 5 ) ] ], + // Newer than 5 hrs from start should return 14 posts. + 'newer_than_5_hrs' => [ 14, 'newerThan', [ $start->clone()->addHours( 5 ) ] ], + // Newer than or equal to 5 hrs from start should return 15 posts. + 'newer_than_or_equal_to_5_hrs' => [ 15, 'newerThanOrEqualTo', [ $start->clone()->addHours( 5 ) ] ], + // Older than the middle post should return 10 posts. + 'older_than_middle' => [ 10, 'olderThan', [ $start->clone()->addHours( 10 ) ] ], + // Newer than the middle post should return 10 posts. + 'newer_than_middle' => [ 10, 'newerThanOrEqualTo', [ $start->clone()->addHours( 10 ) ] ], + ]; + } + /** * Get a random post ID, ensures the post ID is not the last in the set. * From f99332dcb162907c5aadf5baa7c1280a9c9444cf Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Fri, 29 Sep 2023 15:48:13 -0400 Subject: [PATCH 13/32] Change the composer type to library for all packages --- src/mantle/application/composer.json | 2 +- src/mantle/assets/composer.json | 2 +- src/mantle/auth/composer.json | 2 +- src/mantle/blocks/composer.json | 2 +- src/mantle/cache/composer.json | 2 +- src/mantle/config/composer.json | 2 +- src/mantle/console/composer.json | 2 +- src/mantle/container/composer.json | 2 +- src/mantle/contracts/composer.json | 2 +- src/mantle/database/composer.json | 2 +- src/mantle/events/composer.json | 2 +- src/mantle/facade/composer.json | 2 +- src/mantle/faker/composer.json | 2 +- src/mantle/filesystem/composer.json | 2 +- src/mantle/http-client/composer.json | 2 +- src/mantle/http/composer.json | 2 +- src/mantle/log/composer.json | 2 +- src/mantle/new-relic/composer.json | 2 +- src/mantle/query-monitor/composer.json | 2 +- src/mantle/queue/composer.json | 2 +- src/mantle/rest-api/composer.json | 2 +- src/mantle/scheduling/composer.json | 2 +- src/mantle/support/composer.json | 2 +- src/mantle/testing/composer.json | 2 +- src/mantle/testkit/composer.json | 2 +- src/mantle/view/composer.json | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/mantle/application/composer.json b/src/mantle/application/composer.json index 96348f72..af2b9b94 100644 --- a/src/mantle/application/composer.json +++ b/src/mantle/application/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/application", "description": "The Mantle Framework Application Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/assets/composer.json b/src/mantle/assets/composer.json index 33631fb7..23467eb3 100644 --- a/src/mantle/assets/composer.json +++ b/src/mantle/assets/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/assets", "description": "The Mantle Framework Asset Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/auth/composer.json b/src/mantle/auth/composer.json index 1acfa867..eb7291e5 100644 --- a/src/mantle/auth/composer.json +++ b/src/mantle/auth/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/auth", "description": "The Mantle Framework Auth Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/blocks/composer.json b/src/mantle/blocks/composer.json index 8b1703a8..f7adf7f5 100644 --- a/src/mantle/blocks/composer.json +++ b/src/mantle/blocks/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/blocks", "description": "The Mantle Framework Blocks Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/cache/composer.json b/src/mantle/cache/composer.json index 32ead84b..0f19d55f 100644 --- a/src/mantle/cache/composer.json +++ b/src/mantle/cache/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/cache", "description": "The Mantle Framework Cache Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/config/composer.json b/src/mantle/config/composer.json index da9d3d14..769be096 100644 --- a/src/mantle/config/composer.json +++ b/src/mantle/config/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/config", "description": "The Mantle Framework Config Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/console/composer.json b/src/mantle/console/composer.json index f7bc086f..01dfce03 100644 --- a/src/mantle/console/composer.json +++ b/src/mantle/console/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/console", "description": "The Mantle Framework Console Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/container/composer.json b/src/mantle/container/composer.json index 66f631d4..c3aa2104 100644 --- a/src/mantle/container/composer.json +++ b/src/mantle/container/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/container", "description": "The Mantle Framework Container Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/contracts/composer.json b/src/mantle/contracts/composer.json index 6e0dd1d0..2d24d9b4 100644 --- a/src/mantle/contracts/composer.json +++ b/src/mantle/contracts/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/contracts", "description": "The Mantle Framework Contracts Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/database/composer.json b/src/mantle/database/composer.json index 218f8076..f5ee6edc 100644 --- a/src/mantle/database/composer.json +++ b/src/mantle/database/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/database", "description": "The Mantle Framework Database Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/events/composer.json b/src/mantle/events/composer.json index 41fbaab1..04f0cae1 100644 --- a/src/mantle/events/composer.json +++ b/src/mantle/events/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/events", "description": "The Mantle Framework Events Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/facade/composer.json b/src/mantle/facade/composer.json index e18192c4..c13afa74 100644 --- a/src/mantle/facade/composer.json +++ b/src/mantle/facade/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/facade", "description": "The Mantle Framework Facade Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/faker/composer.json b/src/mantle/faker/composer.json index d787bd33..b9b983ad 100644 --- a/src/mantle/faker/composer.json +++ b/src/mantle/faker/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/faker", "description": "The Mantle Framework Faker Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/filesystem/composer.json b/src/mantle/filesystem/composer.json index 81458456..a98a2f14 100644 --- a/src/mantle/filesystem/composer.json +++ b/src/mantle/filesystem/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/filesystem", "description": "The Mantle Framework Filesystem Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/http-client/composer.json b/src/mantle/http-client/composer.json index c1a0e710..bc39b927 100644 --- a/src/mantle/http-client/composer.json +++ b/src/mantle/http-client/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/http-client", "description": "The Mantle Framework Http Client Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/wp-concurrent-remote-requests": "^1.0.2", diff --git a/src/mantle/http/composer.json b/src/mantle/http/composer.json index 6a1c4af4..d2013e91 100644 --- a/src/mantle/http/composer.json +++ b/src/mantle/http/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/http", "description": "The Mantle Framework Http Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "illuminate/view": "^9.52.15", diff --git a/src/mantle/log/composer.json b/src/mantle/log/composer.json index f9a713a5..101393c4 100644 --- a/src/mantle/log/composer.json +++ b/src/mantle/log/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/log", "description": "The Mantle Framework Log Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/new-relic/composer.json b/src/mantle/new-relic/composer.json index 024b1d47..105d2f85 100644 --- a/src/mantle/new-relic/composer.json +++ b/src/mantle/new-relic/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/new-relic", "description": "The Mantle Framework New Relic Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/query-monitor/composer.json b/src/mantle/query-monitor/composer.json index 05b8bc0c..67cdbd05 100644 --- a/src/mantle/query-monitor/composer.json +++ b/src/mantle/query-monitor/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/query-monitor", "description": "The Mantle Framework Query Monitor Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/queue/composer.json b/src/mantle/queue/composer.json index 68874ed5..b8dd169a 100644 --- a/src/mantle/queue/composer.json +++ b/src/mantle/queue/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/queue", "description": "The Mantle Framework Queue Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/rest-api/composer.json b/src/mantle/rest-api/composer.json index e0375b7b..d467ecaf 100644 --- a/src/mantle/rest-api/composer.json +++ b/src/mantle/rest-api/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/rest-api", "description": "The Mantle Framework REST API Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/scheduling/composer.json b/src/mantle/scheduling/composer.json index 150201d2..2ca5d600 100644 --- a/src/mantle/scheduling/composer.json +++ b/src/mantle/scheduling/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/scheduling", "description": "The Mantle Framework Scheduling Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/support/composer.json b/src/mantle/support/composer.json index daa6e599..c42a6e30 100644 --- a/src/mantle/support/composer.json +++ b/src/mantle/support/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/support", "description": "The Mantle Framework Support Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/testing/composer.json b/src/mantle/testing/composer.json index 92628574..6b8f9083 100644 --- a/src/mantle/testing/composer.json +++ b/src/mantle/testing/composer.json @@ -2,7 +2,7 @@ "name": "mantle-framework/testing", "description": "The Mantle Framework Testing Package", "keywords": ["testing", "mantle"], - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/testkit/composer.json b/src/mantle/testkit/composer.json index 8a87bf62..5049aa74 100644 --- a/src/mantle/testkit/composer.json +++ b/src/mantle/testkit/composer.json @@ -2,7 +2,7 @@ "name": "mantle-framework/testkit", "description": "The Mantle Framework Teskit Package", "keywords": ["testing", "mantle"], - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", diff --git a/src/mantle/view/composer.json b/src/mantle/view/composer.json index 1a7ddb8f..2d53342e 100644 --- a/src/mantle/view/composer.json +++ b/src/mantle/view/composer.json @@ -1,7 +1,7 @@ { "name": "mantle-framework/view", "description": "The Mantle Framework View Package", - "type": "project", + "type": "library", "require": { "php": "^8.0", "alleyinteractive/composer-wordpress-autoloader": "^1.0", From 9fdd5169cd1bc58c8b067cd15b59cba9b05c1b2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:38:45 -0400 Subject: [PATCH 14/32] Update phpstan/phpstan requirement from 1.10.33 to 1.10.35 (#457) Updates the requirements on [phpstan/phpstan](https://github.com/phpstan/phpstan) to permit the latest version. - [Release notes](https://github.com/phpstan/phpstan/releases) - [Changelog](https://github.com/phpstan/phpstan/blob/1.11.x/CHANGELOG.md) - [Commits](https://github.com/phpstan/phpstan/compare/1.10.33...1.10.35) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1b14130d..16c776ce 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "mockery/mockery": "^1.6.6", "php-stubs/wp-cli-stubs": "^2.8", "phpstan/phpdoc-parser": "^1.23.1", - "phpstan/phpstan": "1.10.33", + "phpstan/phpstan": "1.10.35", "phpunit/phpunit": "^9.6.10", "predis/predis": "^2.2.0", "squizlabs/php_codesniffer": "^3.7", From 7cb76f596ea5b5a90229818aa0abcea104ebaa39 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 2 Oct 2023 13:05:42 -0400 Subject: [PATCH 15/32] Adding a database collection with found rows (#459) * Adding a database collection and passing along the found_rows to the collection * Allow the value to be passed --- src/mantle/database/query/class-builder.php | 21 +- .../database/query/class-collection.php | 190 ++++++++++++++++++ .../query/class-post-query-builder.php | 53 +++-- .../query/class-term-query-builder.php | 18 +- src/mantle/support/class-collection.php | 6 +- .../query/test-post-query-builder.php | 30 +++ 6 files changed, 283 insertions(+), 35 deletions(-) create mode 100644 src/mantle/database/query/class-collection.php diff --git a/src/mantle/database/query/class-builder.php b/src/mantle/database/query/class-builder.php index da6c1779..969ad838 100644 --- a/src/mantle/database/query/class-builder.php +++ b/src/mantle/database/query/class-builder.php @@ -3,7 +3,7 @@ * Builder class file. * * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid - * phpcs:disable Squiz.Commenting.FunctionComment + * phpcs:disable Squiz.Commenting.VariableComment.Missing, Squiz.Commenting.FunctionComment * phpcs:disable PEAR.Functions.FunctionCallSignature.CloseBracketLine, PEAR.Functions.FunctionCallSignature.MultipleArguments, PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket * * @package Mantle @@ -26,8 +26,6 @@ use Mantle\Support\Str; use Mantle\Support\Traits\Conditionable; -use function Mantle\Support\Helpers\collect; - /** * Builder Query Builder * @@ -41,9 +39,9 @@ abstract class Builder { /** * Model to build on. * - * @var string[]|string + * @var class-string|array> */ - protected $model; + protected array|string $model; /** * Result limit per-page. @@ -132,9 +130,9 @@ abstract class Builder { /** * Storage of the found rows for a query. * - * @var int + * @var int|null */ - protected int $found_rows = 0; + protected ?int $found_rows = 0; /** * Relationships to eager load. @@ -914,16 +912,13 @@ public function __call( $method, $args ) { /** * Collect all the model object names in an associative Collection. * - * @return Collection Collection with object names as keys and model - * class names as values. + * @return Collection> Collection of model class names keyed by object name. */ public function get_model_object_names(): Collection { - return collect( (array) $this->model ) + return ( new Collection( (array) $this->model ) ) // @phpstan-ignore-line should return ->combine( $this->model ) ->map( - function ( $model ) { - return $model::get_object_name(); - } + fn ( $model ) => $model::get_object_name(), ) ->flip(); } diff --git a/src/mantle/database/query/class-collection.php b/src/mantle/database/query/class-collection.php new file mode 100644 index 00000000..fa49b958 --- /dev/null +++ b/src/mantle/database/query/class-collection.php @@ -0,0 +1,190 @@ + + */ +class Collection extends Base_Collection { + /** + * Total number of rows found for the query. + * + * @var int|null + */ + public ?int $found_rows = null; + + /** + * Set the total number of rows found for the query. + * + * @param int|null $found_rows Total number of rows found for the query. + * @return static + */ + public function with_found_rows( ?int $found_rows ): static { + $this->found_rows = $found_rows; + + return $this; + } + + /** + * Get the total number of rows found for the query. + * + * @return int|null + */ + public function found_rows(): ?int { + return $this->found_rows; + } + + /** + * Retrieve the models in the collection. + * + * @return \Mantle\Support\Collection> + */ + public function models(): Base_Collection { + return $this->map( fn ( $model ) => $model::class )->values()->unique(); + } + + /** + * Run a map over each of the items. + * + * @template TMapValue + * + * @param callable(TModel, TKey): TMapValue $callback The callback to run. + * @return \Mantle\Support\Collection|static + */ + public function map( callable $callback ) { + $result = parent::map( $callback ); + + if ( $result instanceof self ) { + $result->with_found_rows( $this->found_rows ); + } + + return $result->contains( fn ( $item ) => ! $item instanceof Model ) + ? $result->to_base() + : $result; + } + + /** + * Run an associative map over each of the items. + * + * The callback should return an associative array with a single key/value pair. + * + * @template TMapWithKeysKey of array-key + * @template TMapWithKeysValue + * + * @param callable(TModel, TKey): array $callback The callback to run. + * @return \Mantle\Support\Collection|static + */ + public function map_with_keys( callable $callback ) { + $result = parent::map_with_keys( $callback ); + + if ( $result instanceof self ) { + $result->with_found_rows( $this->found_rows ); + } + + return $result->contains( fn ( $item ) => ! $item instanceof Model ) + ? $result->to_base() + : $result; + } + + /** + * The following methods are intercepted to always return base collections. + */ + + /** + * Count the number of items in the collection by a field or using a callback. + * + * @param (callable(TModel, TKey): array-key)|string|null $count_by + * @return \Mantle\Support\Collection + */ + public function count_by( $count_by = null ) { + return $this->to_base()->count_by( $count_by ); + } + + /** + * Collapse the collection of items into a single array. + * + * @return \Mantle\Support\Collection + */ + public function collapse() { + return $this->to_base()->collapse(); + } + + /** + * Get a flattened array of the items in the collection. + * + * @param int|float $depth + * @return \Mantle\Support\Collection + */ + public function flatten( $depth = INF ) { + return $this->to_base()->flatten( $depth ); + } + + /** + * Flip the items in the collection. + * + * @return \Mantle\Support\Collection + */ + public function flip() { + return $this->to_base()->flip(); + } + + /** + * Get the keys of the collection items. + * + * @return \Mantle\Support\Collection + */ + public function keys() { + return $this->to_base()->keys(); + } + + /** + * Pad collection to the specified length with a value. + * + * @template TPadValue + * + * @param int $size + * @param TPadValue $value + * @return \Mantle\Support\Collection + */ + public function pad( $size, $value ) { + return $this->to_base()->pad( $size, $value ); + } + + /** + * Get an array with the values of a given key. + * + * @param string|array $value + * @param string|null $key + * @return \Mantle\Support\Collection + */ + public function pluck( $value, $key = null ) { + return $this->to_base()->pluck( $value, $key ); + } + + /** + * Zip the collection together with one or more arrays. + * + * @template TZipValue + * + * @param \Mantle\Contracts\Support\Arrayable|iterable ...$items + * @return static> + */ + public function zip( ...$items ) { // @phpstan-ignore-line return + return $this->to_base()->zip( ...$items ); + } +} diff --git a/src/mantle/database/query/class-post-query-builder.php b/src/mantle/database/query/class-post-query-builder.php index c5ad15ae..995f4dbd 100644 --- a/src/mantle/database/query/class-post-query-builder.php +++ b/src/mantle/database/query/class-post-query-builder.php @@ -11,7 +11,7 @@ use Mantle\Database\Model\Term; use Mantle\Database\Query\Concerns\Queries_Dates; use Mantle\Support\Helpers; -use Mantle\Support\Collection; +use RuntimeException; use WP_Term; use function Mantle\Support\Helpers\collect; @@ -117,7 +117,6 @@ public function get_query_args(): array { return array_merge( [ - 'fields' => 'ids', 'ignore_sticky_posts' => true, 'meta_query' => $this->meta_query, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'order' => $order, @@ -130,6 +129,9 @@ public function get_query_args(): array { ], $this->get_date_query_args(), $this->wheres, + [ + 'fields' => 'ids', + ] ); } @@ -148,14 +150,21 @@ public function get(): Collection { fn () => $query->query( $this->get_query_args() ), ); - $this->found_rows = $query->found_posts; - $post_ids = $query->posts; + if ( empty( $query->found_posts ) && count( $query->posts ) > 0 ) { + $this->found_rows = null; + } else { + $this->found_rows = $query->found_posts; + } + + $post_ids = $query->posts; if ( empty( $post_ids ) ) { - return collect(); + return ( new Collection() )->with_found_rows( $this->found_rows ); // @phpstan-ignore-line should return } - $models = $this->get_models( $post_ids ); + $models = $this + ->get_models( $post_ids ) + ->with_found_rows( $this->found_rows ); // Return the models if there are no models or if multiple model instances // are used. Eager loading does not currently support multiple models. @@ -195,11 +204,18 @@ public function count(): int { protected function get_models( array $post_ids ): Collection { if ( is_array( $this->model ) ) { $model_object_types = $this->get_model_object_names(); - return collect( $post_ids ) + + return Collection::from( $post_ids ) ->map( function ( $post_id ) use ( $model_object_types ) { $post_type = \get_post_type( $post_id ); + if ( empty( $model_object_types[ $post_type ] ) ) { + throw new RuntimeException( + "Missing model for object type [{ $post_type }]." + ); + } + if ( empty( $post_type ) ) { return null; } @@ -207,12 +223,13 @@ function ( $post_id ) use ( $model_object_types ) { return $model_object_types[ $post_type ]::find( $post_id ); } ) - ->filter(); - } else { - return collect( $post_ids ) - ->map( [ $this->model, 'find' ] ) - ->filter(); + ->filter() + ->values(); } + + return Collection::from( $post_ids ) + ->map( [ $this->model, 'find' ] ) + ->filter(); } /** @@ -295,6 +312,18 @@ public function orWhereTerm( ...$args ) { return $this->whereTerm( ...$args ); } + /** + * Fetch the query with 'no_found_rows' set to a value. + * + * Setting to 'true' prevents counting all the available rows for a query. + * + * @param bool $value Whether to set 'no_found_rows' to true. + * @return static + */ + public function withNoFoundRows( bool $value = true ): static { + return $this->where( 'no_found_rows', $value ); + } + /** * Dump the SQL query being executed. * diff --git a/src/mantle/database/query/class-term-query-builder.php b/src/mantle/database/query/class-term-query-builder.php index c624dc9d..40eddc3c 100644 --- a/src/mantle/database/query/class-term-query-builder.php +++ b/src/mantle/database/query/class-term-query-builder.php @@ -7,9 +7,6 @@ namespace Mantle\Database\Query; -use Mantle\Support\Collection; -use function Mantle\Support\Helpers\collect; - /** * Term Query Builder * @@ -90,7 +87,6 @@ public function get_query_args(): array { return array_merge( [ - 'fields' => 'ids', 'hide_empty' => false, 'meta_query' => $this->meta_query, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'number' => $this->limit, @@ -100,6 +96,9 @@ public function get_query_args(): array { 'taxonomy' => $taxonomies, ], $this->wheres, + [ + 'fields' => 'ids', + ] ); } @@ -113,18 +112,23 @@ public function get(): Collection { $this->query_hash = spl_object_hash( $query ); + /** + * Fetch the terms IDs for the query. + * + * @var int[] + */ $term_ids = $this->with_clauses( fn (): array => $query->query( $this->get_query_args() ), ); if ( empty( $term_ids ) ) { - return collect(); + return new Collection(); // @phpstan-ignore-line should return } $models = array_map( [ $this->model, 'find' ], $term_ids ); return $this->eager_load_relations( - collect( $models )->filter()->values(), + Collection::from( $models )->filter()->values(), ); } @@ -140,7 +144,7 @@ public function count(): int { $this->query_hash = spl_object_hash( $query ); - return $this->with_clauses( + return (int) $this->with_clauses( fn (): int => (int) $query->query( array_merge( $this->get_query_args(), diff --git a/src/mantle/support/class-collection.php b/src/mantle/support/class-collection.php index 1970e0ed..923a5312 100644 --- a/src/mantle/support/class-collection.php +++ b/src/mantle/support/class-collection.php @@ -446,7 +446,7 @@ public function first( callable $callback = null, $default = null ) { /** * Get a flattened array of the items in the collection. * - * @param int|float $depth + * @param int|float $depth * @return static */ public function flatten( $depth = INF ) { @@ -456,7 +456,7 @@ public function flatten( $depth = INF ) { /** * Flip the items in the collection. * - * @return static + * @return static */ public function flip() { return new static( array_flip( $this->items ) ); @@ -1327,7 +1327,7 @@ public function values() { * @template TZipValue * * @param \Mantle\Contracts\Support\Arrayable|iterable ...$items - * @return static> + * @return static> */ public function zip( ...$items ) { $arrayable_items = array_map( diff --git a/tests/database/query/test-post-query-builder.php b/tests/database/query/test-post-query-builder.php index 73c0ba32..cf4c8a1c 100644 --- a/tests/database/query/test-post-query-builder.php +++ b/tests/database/query/test-post-query-builder.php @@ -527,6 +527,36 @@ public function test_count() { $this->assertEquals( 1, Testable_Post::whereIn( 'id', [ $post_id ] )->count() ); } + public function test_found_rows() { + static::factory()->post->create_many( 25 ); + + $query = Testable_Post::query() + ->take( 10 ) + ->get(); + + $this->assertInstanceOf( \Mantle\Database\Query\Collection::class, $query ); + $this->assertEquals( 10, $query->count() ); + $this->assertEquals( 25, $query->found_rows() ); + + $models = $query->models(); + + $this->assertNotInstanceOf( \Mantle\Database\Query\Collection::class, $models ); + $this->assertEquals( 1, $models->count() ); + } + + public function test_no_found_rows_true() { + static::factory()->post->create_many( 10 ); + + $query = Testable_Post::query() + ->withNoFoundRows() + ->take( 10 ) + ->get(); + + $this->assertInstanceOf( \Mantle\Database\Query\Collection::class, $query ); + $this->assertEquals( 10, $query->count() ); + $this->assertNull( $query->found_rows() ); + } + public function test_post_by_date() { $old_date = Carbon::now( wp_timezone() )->subMonth(); $now = Carbon::now( wp_timezone() ); From 010a5b41ee4937b18450343c97328abd2ad5f8e6 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 2 Oct 2023 13:23:23 -0400 Subject: [PATCH 16/32] Adds a trait to easily silence remote requests during testing (#460) * Remove fake() alias, too broad * Adds a trait to easily silence remote requests during test * Adding some types to Interacts_With_Requests to prevent bad values from slipping through * Adding types * More types for other methods --- .../trait-interacts-with-requests.php | 97 +++++++++++-------- .../trait-prevent-remote-requests.php | 27 ++++++ .../test-interacts-with-external-requests.php | 29 ++++++ 3 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 src/mantle/testing/concerns/trait-prevent-remote-requests.php diff --git a/src/mantle/testing/concerns/trait-interacts-with-requests.php b/src/mantle/testing/concerns/trait-interacts-with-requests.php index 382f1467..6a4a2718 100644 --- a/src/mantle/testing/concerns/trait-interacts-with-requests.php +++ b/src/mantle/testing/concerns/trait-interacts-with-requests.php @@ -2,7 +2,7 @@ /** * Interacts_With_Requests trait file. * - * @phpcs:disable WordPress.NamingConventions.ValidFunctionName + * @phpcs:disable WordPress.NamingConventions.ValidFunctionName, Squiz.Commenting.FunctionComment.SpacingAfterParamType * * @package Mantle */ @@ -10,6 +10,7 @@ namespace Mantle\Testing\Concerns; use Closure; +use Mantle\Contracts\Support\Arrayable; use Mantle\Http_Client\Request; use Mantle\Http_Client\Response; use Mantle\Support\Collection; @@ -26,19 +27,21 @@ /** * Allow Mock HTTP Requests + * + * @mixin \PHPUnit\Framework\TestCase */ trait Interacts_With_Requests { /** * Storage of the callbacks to mock the requests. * - * @var Collection + * @var Collection */ protected Collection $stub_callbacks; /** * Storage of request URLs. * - * @var Collection + * @var Collection */ protected Collection $recorded_requests; @@ -46,21 +49,21 @@ trait Interacts_With_Requests { * Flag to prevent external requests from being made. By default, this is * false. * - * @var Mock_Http_Response|Closure|bool + * @var Mock_Http_Response|callable|bool */ protected mixed $preventing_stray_requests = false; /** * Recorded actual HTTP requests made during the test. * - * @var Collection + * @var Collection */ protected Collection $recorded_actual_requests; /** * Setup the trait. */ - public function interacts_with_requests_set_up() { + public function interacts_with_requests_set_up(): void { $this->stub_callbacks = collect(); $this->recorded_requests = collect(); $this->recorded_actual_requests = collect(); @@ -71,7 +74,7 @@ public function interacts_with_requests_set_up() { /** * Remove the filter to intercept the request. */ - public function interacts_with_requests_tear_down() { + public function interacts_with_requests_tear_down(): void { \remove_filter( 'pre_http_request', [ $this, 'pre_http_request' ], PHP_INT_MAX ); $this->report_stray_requests(); @@ -82,14 +85,14 @@ public function interacts_with_requests_tear_down() { * * @param Mock_Http_Response|\Closure|bool $response A default response or callback to use, boolean otherwise. */ - public function prevent_stray_requests( $response = true ) { + public function prevent_stray_requests( Mock_Http_Response|Closure|bool $response = true ): void { $this->preventing_stray_requests = $response; } /** * Allow stray external requests. */ - public function allow_stray_requests() { + public function allow_stray_requests(): void { $this->preventing_stray_requests = false; } @@ -102,21 +105,30 @@ public function allow_stray_requests() { * information on how this is used, see the `create_stub_request_callback()` method below and the * relevant test for the trait (Mantle\Tests\Testing\Concerns\Test_Interacts_With_Requests). * + * Example: + * + * $this->fake_request(); + * $this->fake_request( 'https://testing.com/*' ); + * $this->fake_request( 'https://testing.com/*' )->with_response_code( 404 )->with_body( 'test body' ); + * $this->fake_request( fn () => Mock_Http_Response::create()->with_body( 'test body' ) ); + * + * @link https://mantle.alley.com/docs/testing/remote-requests#faking-requests Documentation + * * @throws \InvalidArgumentException Thrown on invalid argument. * - * @param Closure|string|array $url_or_callback URL to fake, array of URL and response pairs, or a closure - * that will return a faked response. - * @param Mock_Http_Response|Mock_Http_Sequence $response Optional response object, defaults to creating a 200 response. + * @template TCallableReturn of Mock_Http_Sequence|Mock_Http_Response|Arrayable + * + * @param (callable(string, array): TCallableReturn)|Mock_Http_Response|string|array $url_or_callback URL to fake, array of URL and response pairs, or a closure + * that will return a faked response. + * @param Mock_Http_Response|callable $response Optional response object, defaults to a 200 response with no body. * @return static|Mock_Http_Response */ - public function fake_request( Mock_Http_Response|Mock_Http_Sequence|Closure|string|array|null $url_or_callback = null, Mock_Http_Response|Mock_Http_Sequence|Closure $response = null ) { + public function fake_request( Mock_Http_Response|callable|string|array|null $url_or_callback = null, Mock_Http_Response|callable $response = null ): static|Mock_Http_Response { if ( is_array( $url_or_callback ) ) { $this->stub_callbacks = $this->stub_callbacks->merge( collect( $url_or_callback ) ->map( - function( $response, $url_or_callback ) { - return $this->create_stub_request_callback( $url_or_callback, $response ); - } + fn ( $response, $url_or_callback ) => $this->create_stub_request_callback( $url_or_callback, $response ), ) ); @@ -126,6 +138,7 @@ function( $response, $url_or_callback ) { // Allow a callback to be passed instead. if ( is_callable( $url_or_callback ) ) { $this->stub_callbacks->push( $url_or_callback ); + return $this; } @@ -152,18 +165,6 @@ function( $response, $url_or_callback ) { return $response; } - /** - * Alias for fake_request(). - * - * @param Closure|string|array $url_or_callback URL to fake, array of URL and response pairs, or a closure - * that will return a faked response. - * @param Mock_Http_Response|Mock_Http_Sequence $response Optional response object, defaults to creating a 200 response. - * @return static|Mock_Http_Response - */ - public function fake( Mock_Http_Response|Mock_Http_Sequence|Closure|string|array|null $url_or_callback = null, Mock_Http_Response|Mock_Http_Sequence|Closure $response = null ) { - return $this->fake_request( $url_or_callback, $response ); - } - /** * Filters pre_http_request to intercept the request, mock a response, and * return it. If the response has already been preempted, the preempt will @@ -209,16 +210,31 @@ public function pre_http_request( $preempt, $request_args, $url ) { * * @param string $url Request URL. * @param array $request_args Request arguments. - * @return array|null + * @return array|WP_Error|null */ protected function get_stub_response( $url, $request_args ): array|WP_Error|null { if ( ! $this->stub_callbacks->is_empty() ) { foreach ( $this->stub_callbacks as $callback ) { $response = $callback( $url, $request_args ); - if ( $response instanceof Mock_Http_Response ) { + + if ( $response instanceof Mock_Http_Response || $response instanceof Arrayable ) { return $response->to_array(); } + // Throw an error when an unknown response type is returned from the callback. + if ( $response && ! is_array( $response ) && ! is_wp_error( $response ) ) { + throw new RuntimeException( + sprintf( + 'Unknown response type returned for faked request to [%s]. Expected a (%s|%s|%s|array), got %s.', + $url, + Mock_Http_Response::class, + Arrayable::class, + WP_Error::class, + gettype( $response ) + ), + ); + } + if ( ! is_null( $response ) ) { return $response; } @@ -228,7 +244,7 @@ protected function get_stub_response( $url, $request_args ): array|WP_Error|null if ( false !== $this->preventing_stray_requests ) { $prevent = value( $this->preventing_stray_requests ); - if ( $prevent instanceof Mock_Http_Response ) { + if ( $prevent instanceof Mock_Http_Response || $prevent instanceof Arrayable ) { return $prevent->to_array(); } @@ -277,17 +293,17 @@ protected function store_streamed_response( string $url, array $response, array /** * Retrieve a callback for the stubbed response. * - * @param string $url URL to stub. - * @param Closure|Mock_Http_Response|Mock_Http_Sequence $response Response to send. - * @return Closure + * @param string $url URL to stub. + * @param callable|Mock_Http_Response $response Response to send. + * @return callable */ - protected function create_stub_request_callback( string $url, $response ): Closure { + protected function create_stub_request_callback( string $url, Mock_Http_Response|callable $response ): callable { return function( string $request_url, array $request_args ) use ( $url, $response ) { if ( ! Str::is( Str::start( $url, '*' ), $request_url ) ) { return; } - return $response instanceof Closure || $response instanceof Mock_Http_Sequence + return is_callable( $response ) ? $response( $request_url, $request_args ) : $response; }; @@ -333,9 +349,10 @@ protected function report_stray_requests(): void { * @param int $expected_times Number of times the request should have been * sent, optional. */ - public function assertRequestSent( $url_or_callback = null, int $expected_times = null ) { + public function assertRequestSent( string|callable|null $url_or_callback = null, int $expected_times = null ): void { if ( is_null( $url_or_callback ) ) { PHPUnit::assertTrue( $this->recorded_requests->is_not_empty(), 'A request was made.' ); + return; } @@ -360,7 +377,7 @@ public function assertRequestSent( $url_or_callback = null, int $expected_times * * @param string|callable $url_or_callback URL to check against or callback. */ - public function assertRequestNotSent( $url_or_callback = null ) { + public function assertRequestNotSent( string|callable|null $url_or_callback = null ): void { if ( is_string( $url_or_callback ) ) { $url_or_callback = fn ( $request ) => Str::is( $url_or_callback, $request->url() ); } @@ -377,7 +394,7 @@ public function assertRequestNotSent( $url_or_callback = null ) { * * @return void */ - public function assertNoRequestSent() { + public function assertNoRequestSent(): void { PHPUnit::assertEmpty( $this->recorded_requests, 'Requests were recorded', @@ -390,7 +407,7 @@ public function assertNoRequestSent() { * @param int $count Request count. * @return void */ - public function assertRequestCount( int $count ) { + public function assertRequestCount( int $count ): void { PHPUnit::assertCount( $count, $this->recorded_requests ); } } diff --git a/src/mantle/testing/concerns/trait-prevent-remote-requests.php b/src/mantle/testing/concerns/trait-prevent-remote-requests.php new file mode 100644 index 00000000..a925977a --- /dev/null +++ b/src/mantle/testing/concerns/trait-prevent-remote-requests.php @@ -0,0 +1,27 @@ +prevent_remote_requests ) { + $this->prevent_stray_requests( new Mock_Http_Response() ); + } + } +} diff --git a/tests/testing/concerns/test-interacts-with-external-requests.php b/tests/testing/concerns/test-interacts-with-external-requests.php index 1afd4752..fb655fc7 100644 --- a/tests/testing/concerns/test-interacts-with-external-requests.php +++ b/tests/testing/concerns/test-interacts-with-external-requests.php @@ -1,10 +1,12 @@ fake_request( 'https://testing.com/*' ) ->with_response_code( 404 ) @@ -25,6 +29,8 @@ public function test_fake_request() { ->with_response_code( 500 ) ->with_body( 'fake body' ); + $this->fake_request( 'https://example.com/', Mock_Http_Response::create()->with_body( 'example body' ) ); + $response = wp_remote_get( 'https://testing.com/' ); $this->assertEquals( 'test body', wp_remote_retrieve_body( $response ) ); $this->assertEquals( 404, wp_remote_retrieve_response_code( $response ) ); @@ -32,6 +38,9 @@ public function test_fake_request() { $response = wp_remote_get( 'https://github.com/' ); $this->assertEquals( 'fake body', wp_remote_retrieve_body( $response ) ); $this->assertEquals( 500, wp_remote_retrieve_response_code( $response ) ); + + $response = wp_remote_get( 'https://example.com/' ); + $this->assertEquals( 'example body', wp_remote_retrieve_body( $response ) ); } public function test_fake_all_requests() { @@ -47,6 +56,9 @@ public function test_fake_all_requests() { public function test_fake_callback() { $this->fake_request( function() { + $this->assertIsString( func_get_arg( 0 ) ); + $this->assertIsArray( func_get_arg( 1 ) ); + return Mock_Http_Response::create() ->with_response_code( 123 ) ->with_body( 'apples' ); @@ -229,6 +241,13 @@ public function test_prevent_stray_requests_no_fallback() { Http::get( 'https://example.org/path/' ); } + public function test_prevent_remote_requests_trait() { + // The trait sets up the default response. + $this->assertInstanceOf( Mock_Http_Response::class, $this->preventing_stray_requests ); + + wp_remote_get( 'https://example.com/' ); + } + public function test_file_as_response() { $this->fake_request( fn() => Mock_Http_Response::create()->with_file( MANTLE_PHPUNIT_FIXTURES_PATH . '/images/alley.jpg' ) @@ -262,4 +281,14 @@ public function test_unknown_file_as_response() { Mock_Http_Response::create()->with_file( 'unknown' ) ); } + + public function test_unknown_return_value_from_callback() { + $this->fake_request( + fn () => new DateTime(), + ); + + $this->expectException( RuntimeException::class ); + + Http::get( 'https://example.com/' ); + } } From a206be3a04b89755fae52128903388ed59d157c1 Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 2 Oct 2023 13:36:47 -0400 Subject: [PATCH 17/32] Improve the messaging of assertions (#461) * Improve the messaging for assertions against the database * Improve the messaging of the Interacts_With_Hooks trait assertions * Apply suggestions from code review Co-authored-by: Greg Marshall --------- Co-authored-by: Greg Marshall --- .../testing/concerns/trait-assertions.php | 229 ++++++++---------- .../concerns/trait-interacts-with-hooks.php | 30 ++- .../concerns/test-interacts-with-hooks.php | 10 + 3 files changed, 139 insertions(+), 130 deletions(-) diff --git a/src/mantle/testing/concerns/trait-assertions.php b/src/mantle/testing/concerns/trait-assertions.php index 23512c9d..0c6bc7ef 100644 --- a/src/mantle/testing/concerns/trait-assertions.php +++ b/src/mantle/testing/concerns/trait-assertions.php @@ -2,15 +2,19 @@ /** * This file contains the Assertions trait * + * phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_print_r + * * @package Mantle */ namespace Mantle\Testing\Concerns; +use Mantle\Contracts\Database\Core_Object; use Mantle\Database\Model\Post; use Mantle\Database\Model\Term; use Mantle\Database\Model\User; use PHPUnit\Framework\Assert as PHPUnit; +use WP_Post; use WP_Term; use function Mantle\Support\Helpers\get_term_object; @@ -84,7 +88,7 @@ public static function assertEqualsIgnoreEOL( $expected, $actual ) { * @param array $expected Expected array. * @param array $actual Array to check. */ - public static function assertEqualSets( $expected, $actual ) { + public static function assertEqualSets( $expected, $actual ): void { sort( $expected ); sort( $actual ); PHPUnit::assertEquals( $expected, $actual ); @@ -96,7 +100,7 @@ public static function assertEqualSets( $expected, $actual ) { * @param array $expected Expected array. * @param array $actual Array to check. */ - public static function assertEqualSetsWithIndex( $expected, $actual ) { + public static function assertEqualSetsWithIndex( $expected, $actual ): void { ksort( $expected ); ksort( $actual ); PHPUnit::assertEquals( $expected, $actual ); @@ -107,7 +111,7 @@ public static function assertEqualSetsWithIndex( $expected, $actual ) { * * @param array $array Array to check. */ - public static function assertNonEmptyMultidimensionalArray( $array ) { + public static function assertNonEmptyMultidimensionalArray( $array ): void { PHPUnit::assertTrue( is_array( $array ) ); PHPUnit::assertNotEmpty( $array ); @@ -129,7 +133,7 @@ public static function assertNonEmptyMultidimensionalArray( $array ) { * @param string ...$prop Any number of WP_Query properties that are expected * to be true for the current request. */ - public static function assertQueryTrue( ...$prop ) { + public static function assertQueryTrue( ...$prop ): void { global $wp_query; $all = [ @@ -197,7 +201,7 @@ public static function assertQueryTrue( ...$prop ) { * @param int $id Expected ID. */ public static function assertQueriedObjectId( int $id ): void { - PHPUnit::assertSame( $id, get_queried_object_id() ); + PHPUnit::assertSame( $id, get_queried_object_id(), 'Queried object ID is not the same.' ); } /** @@ -206,44 +210,32 @@ public static function assertQueriedObjectId( int $id ): void { * @param int $id Expected ID. */ public static function assertNotQueriedObjectId( int $id ): void { - PHPUnit::assertNotSame( $id, get_queried_object_id() ); + PHPUnit::assertNotSame( $id, get_queried_object_id(), 'Queried object ID is the same.' ); } /** * Assert that a given object is equivalent to the global queried object. * - * @param Object $object Expected object. + * @param mixed $object Expected object. + * @param bool $strict Whether to assert the same object type or just the same identifying data. */ - public static function assertQueriedObject( mixed $object ): void { + public static function assertQueriedObject( mixed $object, bool $strict = false ): void { $queried_object = get_queried_object(); - // First, assert the same object types. - PHPUnit::assertInstanceOf( get_class( $object ), $queried_object ); + // Assert the same object types if strict mode. + if ( $strict ) { + PHPUnit::assertInstanceOf( get_class( $object ), $queried_object ); + } // Next, assert identifying data about the object. - switch ( true ) { - case $object instanceof Post: - case $object instanceof User: - PHPUnit::assertSame( $object->id(), $queried_object->ID ); - break; - - case $object instanceof Term: - PHPUnit::assertSame( $object->id(), $queried_object->term_id ); - break; - - case $object instanceof \WP_Post: - case $object instanceof \WP_User: - PHPUnit::assertSame( $object->ID, $queried_object->ID ); - break; - - case $object instanceof \WP_Term: - PHPUnit::assertSame( $object->term_id, $queried_object->term_id ); - break; - - case $object instanceof \WP_Post_Type: - PHPUnit::assertSame( $object->name, $queried_object->name ); - break; - } + match ( true ) { + $object instanceof Post || $object instanceof User => PHPUnit::assertSame( $object->id(), $queried_object->ID, 'Queried object ID is not the same.' ), + $object instanceof Term => PHPUnit::assertSame( $object->id(), $queried_object->term_id, 'Queried object ID is not the same.' ), + $object instanceof \WP_Post || $object instanceof \WP_User => PHPUnit::assertSame( $object->ID, $queried_object->ID, 'Queried object ID is not the same.' ), + $object instanceof \WP_Term => PHPUnit::assertSame( $object->term_id, $queried_object->term_id, 'Queried object ID is not the same.' ), + $object instanceof \WP_Post_Type => PHPUnit::assertSame( $object->name, $queried_object->name, 'Queried object name is not the same.' ), + default => PHPUnit::fail( 'Unknown object type.' ), + }; } /** @@ -254,36 +246,21 @@ public static function assertQueriedObject( mixed $object ): void { public static function assertNotQueriedObject( mixed $object ): void { $queried_object = get_queried_object(); - switch ( true ) { - case $object instanceof Post: - case $object instanceof User: - PHPUnit::assertNotSame( $object->id(), $queried_object->ID ); - break; - - case $object instanceof Term: - PHPUnit::assertNotSame( $object->id(), $queried_object->term_id ); - break; - - case $object instanceof \WP_Post: - case $object instanceof \WP_User: - PHPUnit::assertNotSame( $object->ID, $queried_object->ID ); - break; - - case $object instanceof \WP_Term: - PHPUnit::assertNotSame( $object->term_id, $queried_object->term_id ); - break; - - case $object instanceof \WP_Post_Type: - PHPUnit::assertNotSame( $object->name, $queried_object->name ); - break; - } + match ( true ) { + $object instanceof Post || $object instanceof User => PHPUnit::assertNotSame( $object->id(), $queried_object->ID, 'Queried object ID is the same.' ), + $object instanceof Term => PHPUnit::assertNotSame( $object->id(), $queried_object->term_id, 'Queried object ID is the same.' ), + $object instanceof \WP_Post || $object instanceof \WP_User => PHPUnit::assertNotSame( $object->ID, $queried_object->ID, 'Queried object ID is the same.' ), + $object instanceof \WP_Term => PHPUnit::assertNotSame( $object->term_id, $queried_object->term_id, 'Queried object ID is the same.' ), + $object instanceof \WP_Post_Type => PHPUnit::assertNotSame( $object->name, $queried_object->name, 'Queried object name is the same.' ), + default => PHPUnit::fail( 'Unknown object type.' ), + }; } /** * Assert that the queried object is null. */ public static function assertQueriedObjectNull(): void { - PHPUnit::assertNull( get_queried_object(), 'Expected queried object to be null.' ); + PHPUnit::assertNull( get_queried_object(), 'Queried object is not null.' ); } /** @@ -291,18 +268,19 @@ public static function assertQueriedObjectNull(): void { * * @param array $arguments Arguments to query against. */ - public function assertPostExists( array $arguments ) { - $posts = \get_posts( - array_merge( - [ - 'fields' => 'ids', - 'posts_per_page' => 1, - ], - $arguments - ) + public function assertPostExists( array $arguments ): void { + $arguments = array_merge( + [ + 'fields' => 'ids', + 'posts_per_page' => 1, + ], + $arguments ); - PHPUnit::assertNotEmpty( $posts ); + PHPUnit::assertNotEmpty( + \get_posts( $arguments ), + "Post not found with arguments: \n" . print_r( $arguments, true ), + ); } /** @@ -310,18 +288,19 @@ public function assertPostExists( array $arguments ) { * * @param array $arguments Arguments to query against. */ - public function assertPostDoesNotExists( array $arguments ) { - $posts = \get_posts( - array_merge( - [ - 'fields' => 'ids', - 'posts_per_page' => 1, - ], - $arguments - ) + public function assertPostDoesNotExists( array $arguments ): void { + $arguments = array_merge( + [ + 'fields' => 'ids', + 'posts_per_page' => 1, + ], + $arguments ); - PHPUnit::assertEmpty( $posts ); + PHPUnit::assertEmpty( + \get_posts( $arguments ), + "Post found with arguments: \n" . print_r( $arguments, true ), + ); } /** @@ -329,19 +308,20 @@ public function assertPostDoesNotExists( array $arguments ) { * * @param array $arguments Arguments to query against. */ - public function assertTermExists( array $arguments ) { - $terms = \get_terms( - array_merge( - [ - 'fields' => 'ids', - 'count' => 1, - 'hide_empty' => false, - ], - $arguments - ) + public function assertTermExists( array $arguments ): void { + $arguments = array_merge( + [ + 'fields' => 'ids', + 'count' => 1, + 'hide_empty' => false, + ], + $arguments ); - PHPUnit::assertNotEmpty( $terms ); + PHPUnit::assertNotEmpty( + \get_terms( $arguments ), + "Term not found with arguments: \n" . print_r( $arguments, true ), + ); } /** @@ -349,19 +329,20 @@ public function assertTermExists( array $arguments ) { * * @param array $arguments Arguments to query against. */ - public function assertTermDoesNotExists( array $arguments ) { - $terms = \get_terms( - array_merge( - [ - 'fields' => 'ids', - 'count' => 1, - 'hide_empty' => false, - ], - $arguments - ) + public function assertTermDoesNotExists( array $arguments ): void { + $arguments = array_merge( + [ + 'fields' => 'ids', + 'count' => 1, + 'hide_empty' => false, + ], + $arguments ); - PHPUnit::assertEmpty( $terms ); + PHPUnit::assertEmpty( + \get_terms( $arguments ), + "Term found with arguments: \n" . print_r( $arguments, true ), + ); } /** @@ -370,17 +351,18 @@ public function assertTermDoesNotExists( array $arguments ) { * @param array $arguments Arguments to query against. */ public function assertUserExists( array $arguments ) { - $users = \get_users( - array_merge( - [ - 'fields' => 'ids', - 'count' => 1, - ], - $arguments - ) + $arguments = array_merge( + [ + 'fields' => 'ids', + 'count' => 1, + ], + $arguments ); - PHPUnit::assertNotEmpty( $users ); + PHPUnit::assertNotEmpty( + \get_users( $arguments ), + "User not found with arguments: \n" . print_r( $arguments, true ), + ); } /** @@ -388,18 +370,19 @@ public function assertUserExists( array $arguments ) { * * @param array $arguments Arguments to query against. */ - public function assertUserDoesNotExists( array $arguments ) { - $users = \get_users( - array_merge( - [ - 'fields' => 'ids', - 'count' => 1, - ], - $arguments - ) + public function assertUserDoesNotExists( array $arguments ): void { + $arguments = array_merge( + [ + 'fields' => 'ids', + 'count' => 1, + ], + $arguments ); - PHPUnit::assertEmpty( $users ); + PHPUnit::assertEmpty( + \get_users( $arguments ), + "User found with arguments: \n" . print_r( $arguments, true ), + ); } /** @@ -433,7 +416,7 @@ protected function get_term_from_argument( $argument ): ?WP_Term { * @param Term|\WP_Term|int $term Term to check. * @return void */ - public function assertPostHasTerm( $post, $term ) { + public function assertPostHasTerm( Post|WP_Post|int $post, Term|WP_Term|int $term ): void { if ( $post instanceof Post ) { $post = $post->id(); } @@ -441,7 +424,7 @@ public function assertPostHasTerm( $post, $term ) { $term = $this->get_term_from_argument( $term ); PHPUnit::assertInstanceOf( \WP_Term::class, $term, 'Term not found to assert against' ); - PHPUnit::assertTrue( \has_term( $term->term_id, $term->taxonomy, $post ) ); + PHPUnit::assertTrue( \has_term( $term->term_id, $term->taxonomy, $post ), 'Term not found on post' ); } /** @@ -453,7 +436,7 @@ public function assertPostHasTerm( $post, $term ) { * @param Term|\WP_Term|int $term Term to check. * @return void */ - public function assertPostNotHasTerm( $post, $term ) { + public function assertPostNotHasTerm( Post|WP_Post|int $post, Term|WP_Term|int $term ): void { if ( $post instanceof Post ) { $post = $post->id(); } @@ -461,7 +444,7 @@ public function assertPostNotHasTerm( $post, $term ) { $term = $this->get_term_from_argument( $term ); if ( $term ) { - PHPUnit::assertFalse( \has_term( $term->term_id, $term->taxonomy, $post ) ); + PHPUnit::assertFalse( \has_term( $term->term_id, $term->taxonomy, $post ), 'Term found on post' ); } } @@ -471,7 +454,7 @@ public function assertPostNotHasTerm( $post, $term ) { * @param Post|\WP_Post|int $post Post to check. * @param Term|\WP_Term|int $term Term to check. */ - public function assertPostsDoesNotHaveTerm( $post, $term ) { + public function assertPostsDoesNotHaveTerm( Post|WP_Post|int $post, Term|WP_Term|int $term ): void { $this->assertPostNotHasTerm( $post, $term ); } } diff --git a/src/mantle/testing/concerns/trait-interacts-with-hooks.php b/src/mantle/testing/concerns/trait-interacts-with-hooks.php index f847fac6..c7ef4750 100644 --- a/src/mantle/testing/concerns/trait-interacts-with-hooks.php +++ b/src/mantle/testing/concerns/trait-interacts-with-hooks.php @@ -20,9 +20,9 @@ trait Interacts_With_Hooks { /** * Storage of the hooks that have been fired. * - * @var array + * @var array */ - protected $hooks_fired = []; + protected array $hooks_fired = []; /** * Expectation Container @@ -49,6 +49,7 @@ function( $value ) { } $this->hooks_fired[ $filter ]++; + return $value; } ); @@ -76,14 +77,25 @@ public function interacts_with_hooks_tear_down(): void { * @return void */ public function assertHookApplied( string $hook, int $count = null ): void { - PHPUnit::assertTrue( ! empty( $this->hooks_fired[ $hook ] ) ); + PHPUnit::assertNotEmpty( + $this->hooks_fired[ $hook ] ?? [], + "Asserted that [{$hook}] was not fired." + ); - if ( null !== $count ) { + if ( $count ) { $times_fired = $this->hooks_fired[ $hook ] ?? 0; + PHPUnit::assertEquals( $count, - $this->hooks_fired[ $hook ], - "Asserted that [{$hook}] was fired {$count} times when only fired {$times_fired} times." + $times_fired, + sprintf( + 'Asserted that [%s] was applied %d %s when only applied %d %s.', + $hook, + $count, + 1 === $count ? 'time' : 'times', + $times_fired, + 1 === $times_fired ? 'time' : 'times', + ), ); } } @@ -95,7 +107,11 @@ public function assertHookApplied( string $hook, int $count = null ): void { * @return void */ public function assertHookNotApplied( string $hook ): void { - PHPUnit::assertTrue( empty( $this->hooks_fired[ $hook ] ) ); + PHPUnit::assertEquals( + 0, + $this->hooks_fired[ $hook ] ?? 0, + "Asserted that [{$hook}] was fired." + ); } /** diff --git a/tests/testing/concerns/test-interacts-with-hooks.php b/tests/testing/concerns/test-interacts-with-hooks.php index a2a749fb..2447a3fc 100644 --- a/tests/testing/concerns/test-interacts-with-hooks.php +++ b/tests/testing/concerns/test-interacts-with-hooks.php @@ -109,4 +109,14 @@ public function test_hook_return_int() { $this->assertIsInt( apply_filters( 'int_hook_to_add', 'not_int' ) ); } + + public function test_hook_applied_event() { + $this->expectApplied( Example_Event::class )->once(); + + $this->app['events']->dispatch( new Example_Event() ); + + $this->assertHookApplied( Example_Event::class, 1 ); + } } + +class Example_Event {} From ad9b0d98a807f596370f37146bffffc1e2df272d Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Mon, 2 Oct 2023 15:07:50 -0400 Subject: [PATCH 18/32] CHANGELOG for 0.12.7 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19409894..c340d16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v0.12.7 - 2023-10-02 + +### Added + +- Adding date query builder for posts. +- Adds a trait to easily silence remote requests during testing. + +### Changed + +- Improve the messaging of assertions when testing. + +### Fixed + +- Ensure that attribute and action methods are deduplicated in service providers. + ## v0.12.6 - 2023-09-06 ### Fixed From ab35a63194e21895257ef19723138bfe89d8f3f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:32:38 -0500 Subject: [PATCH 19/32] Bump stefanzweifel/git-auto-commit-action from 4 to 5 (#469) Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 4 to 5. - [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases) - [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v4...v5) --- updated-dependencies: - dependency-name: stefanzweifel/git-auto-commit-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/update-changelog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index 94df2c33..0cdea233 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -21,7 +21,7 @@ jobs: release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: branch: main commit_message: Update CHANGELOG From 67a3a32b88a0bdb6d5698adae2f3f3688405d6cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:32:50 -0500 Subject: [PATCH 20/32] Update phpstan/phpstan requirement from 1.10.35 to 1.10.41 (#471) Updates the requirements on [phpstan/phpstan](https://github.com/phpstan/phpstan) to permit the latest version. - [Release notes](https://github.com/phpstan/phpstan/releases) - [Changelog](https://github.com/phpstan/phpstan/blob/1.11.x/CHANGELOG.md) - [Commits](https://github.com/phpstan/phpstan/compare/1.10.35...1.10.41) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 16c776ce..efd32400 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "mockery/mockery": "^1.6.6", "php-stubs/wp-cli-stubs": "^2.8", "phpstan/phpdoc-parser": "^1.23.1", - "phpstan/phpstan": "1.10.35", + "phpstan/phpstan": "1.10.41", "phpunit/phpunit": "^9.6.10", "predis/predis": "^2.2.0", "squizlabs/php_codesniffer": "^3.7", From ae0641643652fed5d79b99e54a1c739792b96183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Sz=C3=A9pe?= Date: Tue, 14 Nov 2023 16:34:19 +0100 Subject: [PATCH 21/32] Fix typos (#467) --- CHANGELOG.md | 4 ++-- src/mantle/container/class-bound-method.php | 2 +- src/mantle/contracts/interface-application.php | 2 +- src/mantle/events/trait-wordpress-action.php | 2 +- src/mantle/framework/console/class-hook-usage-command.php | 2 +- .../console/generators/class-block-make-command.php | 4 ++-- src/mantle/framework/events/class-discover-events.php | 2 +- src/mantle/support/class-arr.php | 2 +- src/mantle/support/class-driver-manager.php | 2 +- src/mantle/support/class-reflector.php | 2 +- src/mantle/testing/concerns/trait-interacts-with-cron.php | 2 +- src/mantle/testing/concerns/trait-rsync-installation.php | 6 +++--- tests/support/test-collection.php | 6 +++--- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c340d16e..c9169f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,7 +107,7 @@ No changes, just a re-release to fix a bad tag. ## v0.11.2 - 2023-07-21 -- Add back-support for Wordpress 6.0 when testing. +- Add back-support for WordPress 6.0 when testing. ## v0.11.1 - 2023-05-31 @@ -222,7 +222,7 @@ No changes, just a re-release to fix a bad tag. - Cast the item to an array inside of only_children. - Adding keywords to trigger --dev. - Separate requires based on what they include. -- Compatibility layer for Refresh_Database and Installs_Wordpress. +- Compatibility layer for Refresh_Database and Installs_WordPress. ## v0.6.1 - 2022-09-20 diff --git a/src/mantle/container/class-bound-method.php b/src/mantle/container/class-bound-method.php index 885cc37a..f6d2006d 100644 --- a/src/mantle/container/class-bound-method.php +++ b/src/mantle/container/class-bound-method.php @@ -155,7 +155,7 @@ protected static function get_call_reflector( $callback ) { * Get the dependency for the given call parameter. * * @param Container $container Container instance. - * @param \ReflectionParameter $parameter Reflect Paramater. + * @param \ReflectionParameter $parameter Reflect Parameter. * @param array $parameters Parameters to pass. * @param array $dependencies Class dependencies. * @return void diff --git a/src/mantle/contracts/interface-application.php b/src/mantle/contracts/interface-application.php index 895f4e75..c0772d12 100644 --- a/src/mantle/contracts/interface-application.php +++ b/src/mantle/contracts/interface-application.php @@ -191,7 +191,7 @@ public function booted( callable $callback ): static; public function terminating( callable $callback ): static; /** - * Termine the application. + * Terminate the application. * * @return void */ diff --git a/src/mantle/events/trait-wordpress-action.php b/src/mantle/events/trait-wordpress-action.php index 0f6c1471..628a9e6e 100644 --- a/src/mantle/events/trait-wordpress-action.php +++ b/src/mantle/events/trait-wordpress-action.php @@ -157,7 +157,7 @@ protected function validate_argument_type( $argument, ReflectionParameter $param * to that event will pass the argument down to the callback for the action/filter. * * @param mixed $argument Argument value. - * @param ReflectionParameter $parameter Callback paramater. + * @param ReflectionParameter $parameter Callback parameter. */ $modified_argument = $this->dispatch( 'mantle-typehint-resolve:' . $type->getName(), [ null, $argument, $parameter ] ); diff --git a/src/mantle/framework/console/class-hook-usage-command.php b/src/mantle/framework/console/class-hook-usage-command.php index d326f0f0..5171519b 100644 --- a/src/mantle/framework/console/class-hook-usage-command.php +++ b/src/mantle/framework/console/class-hook-usage-command.php @@ -212,7 +212,7 @@ protected function read_file( string $file ): Collection { } /** - * Detrmine if the cache should be used. + * Determine if the cache should be used. * * @return bool */ diff --git a/src/mantle/framework/console/generators/class-block-make-command.php b/src/mantle/framework/console/generators/class-block-make-command.php index 480f386b..9510c32c 100644 --- a/src/mantle/framework/console/generators/class-block-make-command.php +++ b/src/mantle/framework/console/generators/class-block-make-command.php @@ -309,7 +309,7 @@ protected function get_blocks_path(): string { } /** - * Get the base path for the genereated block. + * Get the base path for the generated block. * * @param string $name The block name. * @return string @@ -328,7 +328,7 @@ protected function get_views_path(): string { } /** - * Get the base path for the genereated block. + * Get the base path for the generated block. * * @param string $namespace The block namespace. * @return string diff --git a/src/mantle/framework/events/class-discover-events.php b/src/mantle/framework/events/class-discover-events.php index 4c3b695f..47feddac 100644 --- a/src/mantle/framework/events/class-discover-events.php +++ b/src/mantle/framework/events/class-discover-events.php @@ -128,7 +128,7 @@ protected static function get_listener_events( $listeners, string $base_path ): } $listener_events[ $listener->name . '@' . $method->name ] = [ - Reflector::get_paramater_class_names( + Reflector::get_parameter_class_names( $method->getParameters()[0] ), $priority, diff --git a/src/mantle/support/class-arr.php b/src/mantle/support/class-arr.php index 26b27312..9614a77b 100644 --- a/src/mantle/support/class-arr.php +++ b/src/mantle/support/class-arr.php @@ -126,7 +126,7 @@ public static function dot( $array, string $prepend = '' ): array { * Get all of the given array except for a specified array of keys. * * @param array $array Array to process. - * @param array|string $keys Keys toi filter by. + * @param array|string $keys Keys to filter by. * @return array */ public static function except( array $array, $keys ): array { diff --git a/src/mantle/support/class-driver-manager.php b/src/mantle/support/class-driver-manager.php index 3fd3e4b3..2b2893f6 100644 --- a/src/mantle/support/class-driver-manager.php +++ b/src/mantle/support/class-driver-manager.php @@ -11,7 +11,7 @@ use InvalidArgumentException; /** - * Driver Manager for managing multiple stores and plugable drivers. + * Driver Manager for managing multiple stores and pluggable drivers. */ abstract class Driver_Manager { /** diff --git a/src/mantle/support/class-reflector.php b/src/mantle/support/class-reflector.php index 6437e6c5..c35b7e6b 100644 --- a/src/mantle/support/class-reflector.php +++ b/src/mantle/support/class-reflector.php @@ -50,7 +50,7 @@ public static function get_parameter_class_name( $parameter ) { * @param \ReflectionParameter $parameter * @return array */ - public static function get_paramater_class_names( $parameter ) { + public static function get_parameter_class_names( $parameter ) { $type = $parameter->getType(); if ( ! $type instanceof ReflectionUnionType ) { diff --git a/src/mantle/testing/concerns/trait-interacts-with-cron.php b/src/mantle/testing/concerns/trait-interacts-with-cron.php index fa8e0720..314a4ddc 100644 --- a/src/mantle/testing/concerns/trait-interacts-with-cron.php +++ b/src/mantle/testing/concerns/trait-interacts-with-cron.php @@ -38,7 +38,7 @@ public function assertInCronQueue( string $action, array $args = [] ): void { } /** - * Assert tha an action is not in a cron queue. + * Assert that an action is not in a cron queue. * * @param string $action Action hook of the event. * @param array $args Arguments for the cron queue event. diff --git a/src/mantle/testing/concerns/trait-rsync-installation.php b/src/mantle/testing/concerns/trait-rsync-installation.php index ca5589d2..7f3c2bea 100644 --- a/src/mantle/testing/concerns/trait-rsync-installation.php +++ b/src/mantle/testing/concerns/trait-rsync-installation.php @@ -136,7 +136,7 @@ public function maybe_rsync( string $to = null, string $from = null ): static { * Maybe rsync the codebase to the wp-content within WordPress. * * Will attempt to locate the wp-content directory relative to the current - * directory. As a fallback, it will assumme it is being called from either + * directory. As a fallback, it will assume it is being called from either * /wp-content/plugin/:plugin/tests OR /wp-content/themes/:theme/tests. Will * rsync the codebase from the wp-content level to the root of the WordPress * installation. Also will attempt to locate the wp-content directory relative @@ -349,7 +349,7 @@ protected function perform_rsync_testsuite() { // Install WordPress at the base installation if it doesn't exist yet. if ( ! is_dir( $base_install_path ) || ! is_file( "{$base_install_path}/wp-load.php" ) ) { Utils::info( - "Installating WordPress at {$base_install_path} ...", + "Installing WordPress at {$base_install_path} ...", 'Install Rsync' ); @@ -484,7 +484,7 @@ protected function get_phpunit_command(): string { // Use the first argument and translate it to the rsync-ed path. $executable = $this->translate_location( $args[0] ); - // Attempt to fallback to the phpunit binrary reference in PHP_SELF. This + // Attempt to fallback to the phpunit binary reference in PHP_SELF. This // would be the one used to invoke the current script. With that, we can // translate it to the new location in the rsync-ed WordPress // installation. diff --git a/tests/support/test-collection.php b/tests/support/test-collection.php index 66d4c0aa..1db26c81 100644 --- a/tests/support/test-collection.php +++ b/tests/support/test-collection.php @@ -77,8 +77,8 @@ public function testFirstWhere($collection) $this->assertSame('book', $data->first_where('material', 'paper')['type']); $this->assertSame('gasket', $data->first_where('material', 'rubber')['type']); - $this->assertNull($data->first_where('material', 'nonexistant')); - $this->assertNull($data->first_where('nonexistant', 'key')); + $this->assertNull($data->first_where('material', 'nonexistent')); + $this->assertNull($data->first_where('nonexistent', 'key')); } /** @@ -3623,7 +3623,7 @@ public function testCollectionFromTraversableWithKeys($collection) /** * @dataProvider collectionClassProvider */ - public function testSplitCollectionWithADivisableCount($collection) + public function testSplitCollectionWithADivisibleCount($collection) { $data = new $collection(['a', 'b', 'c', 'd']); From 5c37b70f86aa991bdad58302ee9df96d64e4972d Mon Sep 17 00:00:00 2001 From: Sean Fisher Date: Tue, 14 Nov 2023 10:35:47 -0500 Subject: [PATCH 22/32] Ensure factories can be used with data providers (#462) * Overhauling the generics of factories to return the proper type * Adding a global constant to make testing easier * Include some functionality from core * Ensure that post factories work in data providers * Switch to get_comment_delimited_block_content() to compile blocks * Ensure that a new line is added before/after content --- phpcs.xml | 1 + .../class-factory-service-provider.php | 2 +- .../factory/class-attachment-factory.php | 9 ++- .../database/factory/class-blog-factory.php | 8 ++- .../factory/class-comment-factory.php | 6 +- .../factory/class-factory-container.php | 65 +++++++++++++------ src/mantle/database/factory/class-factory.php | 20 +++--- .../database/factory/class-fluent-factory.php | 12 ++-- .../factory/class-network-factory.php | 8 ++- .../database/factory/class-post-factory.php | 11 +++- .../database/factory/class-term-factory.php | 8 ++- .../database/factory/class-user-factory.php | 8 ++- .../database/model/class-attachment.php | 3 +- src/mantle/database/model/class-comment.php | 2 + src/mantle/database/model/class-model.php | 3 + src/mantle/database/model/class-post.php | 3 + src/mantle/database/model/class-site.php | 2 + src/mantle/database/model/class-term.php | 1 + src/mantle/database/model/class-user.php | 2 + .../model/concerns/trait-has-factory.php | 8 ++- src/mantle/faker/class-faker-provider.php | 20 ++---- src/mantle/testing/wordpress-bootstrap.php | 8 +++ tests/database/factory/test-factory.php | 6 +- .../factory/test-unit-testing-factory.php | 32 +++++++-- 24 files changed, 172 insertions(+), 76 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index eb25c772..42444f7c 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -28,6 +28,7 @@ + diff --git a/src/mantle/database/class-factory-service-provider.php b/src/mantle/database/class-factory-service-provider.php index 23d948af..7ec98f2f 100644 --- a/src/mantle/database/class-factory-service-provider.php +++ b/src/mantle/database/class-factory-service-provider.php @@ -46,7 +46,7 @@ function ( $app, $parameters ) { $locale = config( 'app.faker_locale', Factory::DEFAULT_LOCALE ); if ( ! isset( static::$fakers[ $locale ] ) ) { - static::$fakers[ $locale ] = Factory::create(); + static::$fakers[ $locale ] = Factory::create( $locale ); static::$fakers[ $locale ]->addProvider( new Faker_Provider( static::$fakers[ $locale ] ) diff --git a/src/mantle/database/factory/class-attachment-factory.php b/src/mantle/database/factory/class-attachment-factory.php index 4363966b..d5018746 100644 --- a/src/mantle/database/factory/class-attachment-factory.php +++ b/src/mantle/database/factory/class-attachment-factory.php @@ -8,7 +8,6 @@ namespace Mantle\Database\Factory; use Closure; -use Faker\Generator; use Mantle\Contracts\Database\Core_Object; use Mantle\Database\Model\Attachment; use RuntimeException; @@ -19,7 +18,11 @@ /** * Attachment Factory * - * @template TObject of \Mantle\Database\Model\Attachment + * @template TModel of \Mantle\Database\Model\Attachment + * @template TObject of \WP_Post + * @template TReturnValue + * + * @extends Factory */ class Attachment_Factory extends Post_Factory { use Concerns\Generates_Images; @@ -27,7 +30,7 @@ class Attachment_Factory extends Post_Factory { /** * Model to use when creating objects. * - * @var class-string + * @var class-string */ protected string $model = Attachment::class; diff --git a/src/mantle/database/factory/class-blog-factory.php b/src/mantle/database/factory/class-blog-factory.php index f2a76ff7..29ed305c 100644 --- a/src/mantle/database/factory/class-blog-factory.php +++ b/src/mantle/database/factory/class-blog-factory.php @@ -14,13 +14,17 @@ /** * Blog Factory * - * @template TObject of \Mantle\Database\Model\Site + * @template TModel of \Mantle\Database\Model\Site + * @template TObject of \WP_Site + * @template TReturnValue + * + * @extends Factory */ class Blog_Factory extends Factory { /** * Model to use when creating objects. * - * @var class-string + * @var class-string */ protected string $model = Site::class; diff --git a/src/mantle/database/factory/class-comment-factory.php b/src/mantle/database/factory/class-comment-factory.php index 3f8abb31..b71189db 100644 --- a/src/mantle/database/factory/class-comment-factory.php +++ b/src/mantle/database/factory/class-comment-factory.php @@ -14,7 +14,11 @@ /** * Term Factory * - * @template TObject of \Mantle\Database\Model\Comment + * @template TModel of \Mantle\Database\Model\Comment + * @template TObject of \WP_Comment + * @template TReturnValue + * + * @extends Factory */ class Comment_Factory extends Factory { /** diff --git a/src/mantle/database/factory/class-factory-container.php b/src/mantle/database/factory/class-factory-container.php index 1e324cef..ff2e8923 100644 --- a/src/mantle/database/factory/class-factory-container.php +++ b/src/mantle/database/factory/class-factory-container.php @@ -7,7 +7,9 @@ namespace Mantle\Database\Factory; +use Faker\Generator; use Mantle\Contracts\Container; +use Mantle\Faker\Faker_Provider; /** * Collect all the Database Factories for IDE Support @@ -21,72 +23,72 @@ class Factory_Container { /** * Attachment Factory * - * @var Attachment_Factory<\WP_Post|\Mantle\Database\Model\Attachment> + * @var Attachment_Factory<\Mantle\Database\Model\Attachment, \WP_Post, \WP_Post> */ - public $attachment; + public Attachment_Factory $attachment; /** * Blog Factory * - * @var Blog_Factory<\WP_Site|\Mantle\Database\Model\Site> + * @var Blog_Factory<\Mantle\Database\Model\Site, \WP_Site, \WP_Site> */ - public $blog; + public Blog_Factory $blog; /** * Category Factory * - * @var Term_Factory<\WP_Term|\Mantle\Database\Model\Term> + * @var Term_Factory<\Mantle\Database\Model\Term, \WP_Term, \WP_Term> */ - public $category; + public Term_Factory $category; /** * Comment Factory * - * @var Comment_Factory<\WP_Comment> + * @var Comment_Factory<\Mantle\Database\Model\Comment, \WP_Comment, \WP_Comment> */ - public $comment; + public Comment_Factory $comment; /** * Network Factory * - * @var Network_Factory<\WP_Network> + * @var Network_Factory */ - public $network; + public Network_Factory $network; /** * Page Factory * - * @var Post_Factory<\WP_Post|\Mantle\Database\Model\Post> + * @var Post_Factory<\Mantle\Database\Model\Post, \WP_Post, \WP_Post> */ public $page; /** * Post Factory * - * @var Post_Factory<\WP_Post|\Mantle\Database\Model\Post> + * @var Post_Factory<\Mantle\Database\Model\Post, \WP_Post, \WP_Post> */ - public $post; + public Post_Factory $post; /** * Tag Factory * - * @var Term_Factory<\WP_Term|\Mantle\Database\Model\Term> + * @var Term_Factory<\Mantle\Database\Model\Term, \WP_Term, \WP_Term> */ - public $tag; + public Term_Factory $tag; /** * Term Factory (alias for Tag Factory). * - * @var Term_Factory<\WP_Term|\Mantle\Database\Model\Term> + * @var Term_Factory<\Mantle\Database\Model\Term, \WP_Term, \WP_Term> */ - public $term; + public Term_Factory $term; /** * User Factory * - * @var User_Factory<\WP_User|\Mantle\Database\Model\User> + * @var User_Factory<\Mantle\Database\Model\User, \WP_User, \WP_User> */ - public $user; + public User_Factory $user; /** * Constructor. @@ -94,6 +96,8 @@ class Factory_Container { * @param Container $container Container instance. */ public function __construct( Container $container ) { + $this->setup_faker( $container ); + $this->attachment = $container->make( Attachment_Factory::class ); $this->category = $container->make( Term_Factory::class, [ 'taxonomy' => 'category' ] ); $this->comment = $container->make( Comment_Factory::class ); @@ -108,4 +112,27 @@ public function __construct( Container $container ) { $this->network = $container->make( Network_Factory::class ); } } + + /** + * Set up the Faker instance in the container. + * + * Primarily used when faker/factory is called from a data provider and the + * application hasn't been setup yet. + * + * @param Container $container Container instance. + */ + protected function setup_faker( Container $container ): void { + $container->singleton_if( + Generator::class, + function () { + $generator = \Faker\Factory::create(); + + $generator->unique(); + + $generator->addProvider( new Faker_Provider( $generator ) ); + + return $generator; + }, + ); + } } diff --git a/src/mantle/database/factory/class-factory.php b/src/mantle/database/factory/class-factory.php index c7471684..767fd590 100644 --- a/src/mantle/database/factory/class-factory.php +++ b/src/mantle/database/factory/class-factory.php @@ -24,9 +24,11 @@ /** * Base Factory * - * @template TObject of \Mantle\Database\Model\Model + * @template TModel of \Mantle\Database\Model\Model + * @template TObject + * @template TReturnValue * - * @method \Mantle\Database\Factory\Fluent_Factory count(int $count) + * @method \Mantle\Database\Factory\Fluent_Factory count(int $count) */ abstract class Factory { use Concerns\Resolves_Factories, @@ -74,7 +76,7 @@ abstract public function definition(): array; * Retrieves an object by ID. * * @param int $object_id The object ID. - * @return mixed + * @return TModel|TObject|null */ abstract public function get_object_by_id( int $object_id ); @@ -91,7 +93,7 @@ public function create( array $args = [] ): mixed { /** * Generate models from the factory. * - * @return static + * @return static */ public function as_models() { return tap( @@ -103,7 +105,7 @@ public function as_models() { /** * Generate core WordPress objects from the factory. * - * @return static + * @return static */ public function as_objects() { return tap( @@ -145,8 +147,10 @@ public function without_middleware() { * * @throws \InvalidArgumentException If the model does not extend from the base model class. * - * @param class-string $model The model to use. - * @return static + * @template TNewModel of \Mantle\Database\Model\Model + * + * @param class-string $model The model to use. + * @return static */ public function with_model( string $model ) { // Validate that model extends from the base model class. @@ -210,7 +214,7 @@ public function create_many( int $count, array $args = [] ) { * Creates an object and returns its object. * * @param array $args Optional. The arguments for the object to create. Default is empty array. - * @return TObject The created object. + * @return TReturnValue The created object. */ public function create_and_get( array $args = [] ) { return $this->get_object_by_id( $this->create( $args ) ); diff --git a/src/mantle/database/factory/class-fluent-factory.php b/src/mantle/database/factory/class-fluent-factory.php index 653306c1..8b45d878 100644 --- a/src/mantle/database/factory/class-fluent-factory.php +++ b/src/mantle/database/factory/class-fluent-factory.php @@ -18,7 +18,11 @@ * Extends upon the factory that is included with Mantle (one that is designed * to mirror WordPress) and builds upon it to provide a fluent interface. * - * @template TObject of \Mantle\Database\Model\Model + * @template TModel of \Mantle\Database\Model\Model + * @template TObject + * @template TReturnValue + * + * @extends Factory */ class Fluent_Factory extends Factory { /** @@ -57,7 +61,7 @@ public function count( int $count ): static { * Create one or multiple objects and return the IDs. * * @param array $args Arguments to pass to the factory. - * @return \Mantle\Support\Collection|mixed + * @return \Mantle\Support\Collection|mixed */ public function create( array $args = [] ): mixed { if ( 1 === $this->count ) { @@ -71,7 +75,7 @@ public function create( array $args = [] ): mixed { * Create one or multiple objects and return the objects. * * @param array $args Arguments to pass to the factory. - * @return \Mantle\Support\Collection|TObject + * @return \Mantle\Support\Collection|TReturnValue */ public function create_and_get( array $args = [] ): mixed { if ( 1 === $this->count ) { @@ -98,7 +102,7 @@ public function definition(): array { * Retrieves an object by ID. * * @param mixed $object_id The object ID. - * @return TObject + * @return TReturnValue */ public function get_object_by_id( mixed $object_id ): mixed { return $this->factory->get_object_by_id( $object_id ); diff --git a/src/mantle/database/factory/class-network-factory.php b/src/mantle/database/factory/class-network-factory.php index 65f5b802..f708462f 100644 --- a/src/mantle/database/factory/class-network-factory.php +++ b/src/mantle/database/factory/class-network-factory.php @@ -7,12 +7,14 @@ namespace Mantle\Database\Factory; -use Faker\Generator; - /** * Network Factory * - * @template TObject + * @template TModel + * @template TObject of \WP_Network + * @template TReturnValue + * + * @extends Factory */ class Network_Factory extends Factory { /** diff --git a/src/mantle/database/factory/class-post-factory.php b/src/mantle/database/factory/class-post-factory.php index c69f53c9..1ad971b9 100644 --- a/src/mantle/database/factory/class-post-factory.php +++ b/src/mantle/database/factory/class-post-factory.php @@ -19,7 +19,11 @@ /** * Post Factory * - * @template TObject of \Mantle\Database\Model\Post + * @template TModel of \Mantle\Database\Model\Post + * @template TObject + * @template TReturnValue + * + * @extends Factory */ class Post_Factory extends Factory { use Concerns\With_Meta; @@ -27,7 +31,7 @@ class Post_Factory extends Factory { /** * Model to use when creating objects. * - * @var class-string + * @var class-string */ protected string $model = Post::class; @@ -44,7 +48,7 @@ public function __construct( Generator $faker, public string $post_type = 'post' /** * Create a new factory instance to create posts with a set of terms. * - * @param array|\WP_Term|int|string ...$terms Terms to assign to the post. + * @param array>|\WP_Term|int|string ...$terms Terms to assign to the post. * @return static */ public function with_terms( ...$terms ): static { @@ -172,6 +176,7 @@ public function create_ordered_set( * * @param int $object_id The object ID. * @return Post|WP_Post|null + * @phpstan-return TModel|TObject|null */ public function get_object_by_id( int $object_id ) { return $this->as_models diff --git a/src/mantle/database/factory/class-term-factory.php b/src/mantle/database/factory/class-term-factory.php index 52db8f8c..4d49ecab 100644 --- a/src/mantle/database/factory/class-term-factory.php +++ b/src/mantle/database/factory/class-term-factory.php @@ -16,7 +16,11 @@ /** * Term Factory * - * @template TObject of \Mantle\Database\Model\Term + * @template TModel of \Mantle\Database\Model\Term + * @template TObject of \WP_Term + * @template TReturnValue + * + * @extends Factory */ class Term_Factory extends Factory { use Concerns\With_Meta; @@ -24,7 +28,7 @@ class Term_Factory extends Factory { /** * Model to use when creating objects. * - * @var class-string + * @var class-string */ protected string $model = Term::class; diff --git a/src/mantle/database/factory/class-user-factory.php b/src/mantle/database/factory/class-user-factory.php index ce41b3d4..927057ad 100644 --- a/src/mantle/database/factory/class-user-factory.php +++ b/src/mantle/database/factory/class-user-factory.php @@ -14,7 +14,11 @@ /** * User Factory * - * @template TObject of \Mantle\Database\Model\User + * @template TModel of \Mantle\Database\Model\User + * @template TObject of \WP_User + * @template TReturnValue + * + * @extends Factory */ class User_Factory extends Factory { use Concerns\With_Meta; @@ -22,7 +26,7 @@ class User_Factory extends Factory { /** * Model to use when creating objects. * - * @var class-string + * @var class-string */ protected string $model = User::class; diff --git a/src/mantle/database/model/class-attachment.php b/src/mantle/database/model/class-attachment.php index d92ac3be..5edf4be8 100644 --- a/src/mantle/database/model/class-attachment.php +++ b/src/mantle/database/model/class-attachment.php @@ -7,11 +7,12 @@ namespace Mantle\Database\Model; -use Mantle\Contracts; use Mantle\Facade\Storage; /** * Attachment Model + * + * @method static \Mantle\Database\Factory\Post_Factory factory( array|callable|null $state = null ) */ class Attachment extends Post { /** diff --git a/src/mantle/database/model/class-comment.php b/src/mantle/database/model/class-comment.php index 7df3c992..60fe3c34 100644 --- a/src/mantle/database/model/class-comment.php +++ b/src/mantle/database/model/class-comment.php @@ -12,6 +12,8 @@ /** * Comment Model + * + * @method static \Mantle\Database\Factory\Post_Factory factory( array|callable|null $state = null ) */ class Comment extends Model implements Contracts\Database\Core_Object, Contracts\Database\Model_Meta, Contracts\Database\Updatable { use Meta\Model_Meta, diff --git a/src/mantle/database/model/class-model.php b/src/mantle/database/model/class-model.php index b07ebccb..470b714c 100644 --- a/src/mantle/database/model/class-model.php +++ b/src/mantle/database/model/class-model.php @@ -25,6 +25,8 @@ /** * Database Model * + * @template TModelObject of object + * * @method static \Mantle\Support\Collection all() * @method static static first() * @method static static first_or_fail() @@ -41,6 +43,7 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab Concerns\Has_Aliases, Concerns\Has_Attributes, Concerns\Has_Events, + /** @use Concerns\Has_Factory */ Concerns\Has_Factory, Concerns\Has_Global_Scopes, Concerns\Has_Relationships; diff --git a/src/mantle/database/model/class-post.php b/src/mantle/database/model/class-post.php index fc3c7021..f6d72c87 100644 --- a/src/mantle/database/model/class-post.php +++ b/src/mantle/database/model/class-post.php @@ -17,6 +17,8 @@ /** * Post Model * + * @extends Model<\WP_Post> + * * @property int $comment_count * @property int $ID * @property int $menu_order @@ -49,6 +51,7 @@ * @property string $status Alias to post_status. * @property string $title Alias to post_title. * + * @method static \Mantle\Database\Factory\Post_Factory factory( array|callable|null $state = null ) * @method static \Mantle\Database\Query\Post_Query_Builder anyStatus() * @method static \Mantle\Database\Query\Post_Query_Builder where( string|array $attribute, mixed $value ) * @method static \Mantle\Database\Query\Post_Query_Builder whereId( int $id ) diff --git a/src/mantle/database/model/class-site.php b/src/mantle/database/model/class-site.php index 0d328cfe..bd75aa39 100644 --- a/src/mantle/database/model/class-site.php +++ b/src/mantle/database/model/class-site.php @@ -12,6 +12,8 @@ /** * Site Model + * + * @method static \Mantle\Database\Factory\Post_Factory factory( array|callable|null $state = null ) */ class Site extends Model implements Contracts\Database\Core_Object, Contracts\Database\Updatable { /** diff --git a/src/mantle/database/model/class-term.php b/src/mantle/database/model/class-term.php index 94aa3eef..19fa2771 100644 --- a/src/mantle/database/model/class-term.php +++ b/src/mantle/database/model/class-term.php @@ -22,6 +22,7 @@ * @property string $slug * @property string $taxonomy * + * @method static \Mantle\Database\Factory\Term_Factory factory( array|callable|null $state = null ) * @method static \Mantle\Database\Query\Term_Query_Builder whereId( int $id ) * @method static \Mantle\Database\Query\Term_Query_Builder whereName(string $name) * @method static \Mantle\Database\Query\Term_Query_Builder whereSlug(string $slug) diff --git a/src/mantle/database/model/class-user.php b/src/mantle/database/model/class-user.php index aceb6be7..8a429921 100644 --- a/src/mantle/database/model/class-user.php +++ b/src/mantle/database/model/class-user.php @@ -12,6 +12,8 @@ /** * User Model + * + * @method static \Mantle\Database\Factory\User_Factory factory( array|callable|null $state = null ) */ class User extends Model implements Contracts\Database\Core_Object, Contracts\Database\Model_Meta, Contracts\Database\Updatable { use Meta\Model_Meta, diff --git a/src/mantle/database/model/concerns/trait-has-factory.php b/src/mantle/database/model/concerns/trait-has-factory.php index 04147b46..1ba2a18b 100644 --- a/src/mantle/database/model/concerns/trait-has-factory.php +++ b/src/mantle/database/model/concerns/trait-has-factory.php @@ -12,14 +12,16 @@ use Mantle\Database\Factory\Term_Factory; /** - * Model Database Factory + * Trait to add a factory to a model. + * + * @template TObject */ trait Has_Factory { /** * Create a builder for the model. * * @param array|callable $state Default state array or callable that will be invoked to set state. - * @return \Mantle\Database\Factory\Factory + * @return \Mantle\Database\Factory\Factory */ public static function factory( array|callable|null $state = null ): Factory { $factory = static::new_factory() ?: Factory::factory_for_model( static::class ); @@ -43,7 +45,7 @@ public static function factory( array|callable|null $state = null ): Factory { * * Optional: allows for the model factory to be overridden by application code. * - * @return \Mantle\Database\Factory\Factory|null + * @return \Mantle\Database\Factory\Factory|null */ protected static function new_factory(): ?Factory { return null; diff --git a/src/mantle/faker/class-faker-provider.php b/src/mantle/faker/class-faker-provider.php index 37bd9724..bf1f82d3 100644 --- a/src/mantle/faker/class-faker-provider.php +++ b/src/mantle/faker/class-faker-provider.php @@ -51,22 +51,12 @@ public function paragraph_blocks( int $count = 3, bool $as_text = true ) { * @param array $attributes Attributes for the block. * @return string */ - public static function block( string $block_name, string $content = '', array $attributes = [] ) { - $attributes = ! empty( $attributes ) ? \wp_json_encode( $attributes ) . ' ' : ''; - - if ( empty( $content ) ) { - return sprintf( - '', - $block_name, - $attributes - ); + public static function block( string $block_name, string $content = '', array $attributes = [] ): string { + // Add a newline before and after the content. + if ( ! empty( $content ) ) { + $content = "\n{$content}\n"; } - return sprintf( - '%3$s', - $block_name, - $attributes, - PHP_EOL . $content . PHP_EOL - ); + return get_comment_delimited_block_content( $block_name, $attributes, $content ); } } diff --git a/src/mantle/testing/wordpress-bootstrap.php b/src/mantle/testing/wordpress-bootstrap.php index d46f894d..34932796 100644 --- a/src/mantle/testing/wordpress-bootstrap.php +++ b/src/mantle/testing/wordpress-bootstrap.php @@ -11,6 +11,8 @@ use function Mantle\Testing\tests_add_filter; +defined( 'MANTLE_IS_TESTING' ) || define( 'MANTLE_IS_TESTING', true ); + require_once __DIR__ . '/class-utils.php'; require_once __DIR__ . '/class-wp-die.php'; @@ -174,6 +176,12 @@ // Use the Spy REST Server instead of default. tests_add_filter( 'wp_rest_server_class', [ Utils::class, 'wp_rest_server_class_filter' ], PHP_INT_MAX ); +// Prevent updating translations asynchronously. +tests_add_filter( 'async_update_translation', '__return_false' ); + +// Disable background updates. +tests_add_filter( 'automatic_updater_disabled', '__return_true' ); + // Load WordPress. require_once ABSPATH . '/wp-settings.php'; diff --git a/tests/database/factory/test-factory.php b/tests/database/factory/test-factory.php index 4d983b6b..67cd6992 100644 --- a/tests/database/factory/test-factory.php +++ b/tests/database/factory/test-factory.php @@ -6,11 +6,13 @@ use Mantle\Database\Model; use Mantle\Testing\Framework_Test_Case; +/** + * @group factory + */ class Test_Factory extends Framework_Test_Case { public function test_create_basic_model() { $factory = Testable_Post::factory(); - $this->assertInstanceOf( Factory\Factory::class, $factory ); $this->assertInstanceOf( Factory\Post_Factory::class, $factory ); $post = $factory->create_and_get(); @@ -154,7 +156,7 @@ class Testable_Post extends Model\Post { } /** - * @method static Testable_Post_Factory factory() + * @method static Testable_Post_Factory factory() */ class Testable_Post_With_Factory extends Model\Post { public static $object_name = 'post'; diff --git a/tests/database/factory/test-unit-testing-factory.php b/tests/database/factory/test-unit-testing-factory.php index 7c0c3c3a..54cf3b81 100644 --- a/tests/database/factory/test-unit-testing-factory.php +++ b/tests/database/factory/test-unit-testing-factory.php @@ -14,6 +14,8 @@ * Test case with the focus of testing the unit testing factory that mirrors * WordPress core's factories. The factories here should be drop-in replacements * for core's factories with some sugar on top. + * + * @group factory */ class Test_Unit_Testing_Factory extends Framework_Test_Case { use With_Faker; @@ -21,7 +23,7 @@ class Test_Unit_Testing_Factory extends Framework_Test_Case { public function test_post_factory() { $this->assertInstanceOf( \WP_Post::class, static::factory()->post->create_and_get() ); - $posts = static::factory()->post->create_many( + $post_ids = static::factory()->post->create_many( 10, [ 'post_type' => 'post', @@ -29,12 +31,12 @@ public function test_post_factory() { ] ); - $this->assertCount( 10, $posts ); - foreach ( $posts as $post_id ) { + $this->assertCount( 10, $post_ids ); + foreach ( $post_ids as $post_id ) { $this->assertIsInt( $post_id ); } - $this->assertEquals( 'draft', get_post_status( array_shift( $posts ) ) ); + $this->assertEquals( 'draft', get_post_status( array_shift( $post_ids ) ) ); } public function test_post_create_with_thumbnail() { @@ -93,6 +95,7 @@ public function test_attachment_factory() { $this->shim_test( \WP_Post::class, 'attachment' ); $attachment = static::factory()->attachment->create_and_get(); + $this->assertEquals( 'attachment', get_post_type( $attachment ) ); } @@ -104,7 +107,6 @@ public function test_term_factory() { public function test_blog_factory() { if ( ! is_multisite() ) { $this->markTestSkipped( 'This test requires multisite.' ); - return; } $this->shim_test( \WP_Site::class, 'blog' ); @@ -113,7 +115,6 @@ public function test_blog_factory() { public function test_network_factory() { if ( ! is_multisite() ) { $this->markTestSkipped( 'This test requires multisite.' ); - return; } $this->shim_test( \WP_Network::class, 'network' ); @@ -148,7 +149,7 @@ public function test_comment_factory() { public function test_as_models() { $post = static::factory()->post->as_models()->create_and_get(); - $term = static::factory()->term->as_models()->with_model( Testable_Post_Tag::class )->create_and_get(); + $term = static::factory()->term->with_model( Testable_Post_Tag::class )->as_models()->create_and_get(); $this->assertInstanceOf( Post::class, $post ); $this->assertInstanceOf( Testable_Post_Tag::class, $term ); @@ -271,6 +272,23 @@ protected function shim_test( string $class_name, string $property ) { $this->assertCount( 10, $object_ids ); } + + /** + * @dataProvider dataprovider_factory + */ + public function test_dataprovider_factory( $post ) { + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertStringContainsString( + '