From 000c4747e914617644afd508001838b89dd0cf0d Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Thu, 26 Apr 2018 14:01:55 -0500 Subject: [PATCH 1/5] periods hook extension: Only select needed fields in query The other fields we already have, so there is no reason to pull them from the database. See #762 --- src/classes/hook/extension/periods.php | 9 ++++++++- tests/phpunit/tests/classes/hook/extension/periods.php | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/classes/hook/extension/periods.php b/src/classes/hook/extension/periods.php index 19b80f30..617ad4a3 100644 --- a/src/classes/hook/extension/periods.php +++ b/src/classes/hook/extension/periods.php @@ -357,7 +357,7 @@ protected function get_period_by_reaction( $period = $wpdb->get_row( $wpdb->prepare( " - SELECT *, `period`.`id` AS `id` + SELECT `period`.`id`, `hit`.`date`, `hit`.`id` AS `hit_id` FROM `{$wpdb->wordpoints_hook_periods}` AS `period` INNER JOIN `{$wpdb->wordpoints_hook_hits}` AS `hit` ON `hit`.`id` = period.`hit_id` @@ -383,6 +383,13 @@ protected function get_period_by_reaction( return false; } + $period->signature = $signature; + $period->reaction_mode = $reaction_guid['mode']; + $period->reaction_store = $reaction_guid['store']; + $period->reaction_context_id = wp_json_encode( $reaction_guid['context_id'] ); + $period->reaction_id = $reaction_guid['id']; + $period->action_type = $this->action_type; + wp_cache_set( $cache_key, $period->id, 'wordpoints_hook_period_ids_by_reaction' ); wp_cache_set( $period->id, $period, 'wordpoints_hook_periods' ); diff --git a/tests/phpunit/tests/classes/hook/extension/periods.php b/tests/phpunit/tests/classes/hook/extension/periods.php index ff7a8dda..071ff161 100644 --- a/tests/phpunit/tests/classes/hook/extension/periods.php +++ b/tests/phpunit/tests/classes/hook/extension/periods.php @@ -1640,7 +1640,7 @@ public function is_get_period_by_reaction_query( $sql ) { return false !== strpos( $sql - , "SELECT *, `period`.`id` AS `id` + , "SELECT `period`.`id`, `hit`.`date`, `hit`.`id` AS `hit_id` FROM `{$wpdb->wordpoints_hook_periods}` AS `period` INNER JOIN `{$wpdb->wordpoints_hook_hits}` AS `hit` ON `hit`.`id` = period.`hit_id` From da42ef73e9878ececa2b7047336ae46c9f41f212 Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Fri, 27 Apr 2018 13:26:03 -0500 Subject: [PATCH 2/5] periods hook extension: Indicate nonexistent periods in the cache To avoid querying repeatedly for a period that doesn't exist in the DB. See #762 --- src/classes/hook/extension/periods.php | 11 ++- .../tests/classes/hook/extension/periods.php | 92 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/classes/hook/extension/periods.php b/src/classes/hook/extension/periods.php index 617ad4a3..2c96d7c8 100644 --- a/src/classes/hook/extension/periods.php +++ b/src/classes/hook/extension/periods.php @@ -344,11 +344,15 @@ protected function get_period_by_reaction( $cache_key = wp_json_encode( $reaction_guid ) . "-{$signature}-{$this->action_type}"; // Before we run the query, we try to lookup the ID in the cache. - $period_id = wp_cache_get( $cache_key, 'wordpoints_hook_period_ids_by_reaction' ); + $period_id = wp_cache_get( $cache_key, 'wordpoints_hook_period_ids_by_reaction', false, $found ); // If we found it, we can retrieve the period by ID instead. if ( $period_id ) { return $this->get_period( $period_id ); + } elseif ( $found ) { + // If the cache was set to false, then we have already checked if there + // are any hits for this period, and haven't found any. + return false; } global $wpdb; @@ -380,6 +384,11 @@ protected function get_period_by_reaction( ); if ( ! $period ) { + + // Cache the result anyway, so we know that no periods have been created + // matching this yet, and can avoid re-running this query. + wp_cache_set( $cache_key, false, 'wordpoints_hook_period_ids_by_reaction' ); + return false; } diff --git a/tests/phpunit/tests/classes/hook/extension/periods.php b/tests/phpunit/tests/classes/hook/extension/periods.php index 071ff161..c1cfa5a2 100644 --- a/tests/phpunit/tests/classes/hook/extension/periods.php +++ b/tests/phpunit/tests/classes/hook/extension/periods.php @@ -1571,6 +1571,98 @@ public function test_should_hit_uncached() { $this->assertCount( 1, $test_reactor->hits ); } + /** + * Test caching when there are no matching periods in the DB. + * + * @since 2.4.2 + */ + public function test_should_hit_cache_no_hit() { + + $this->mock_apps(); + + $hooks = wordpoints_hooks(); + + $extensions = $hooks->get_sub_app( 'extensions' ); + $extensions->register( $this->extension_slug, $this->extension_class ); + $extensions->register( 'blocker', 'WordPoints_PHPUnit_Mock_Hook_Extension' ); + + // Get this first so that it will run before the blocker extension. + $extensions->get( $this->extension_slug ); + + // Set up the blocker extension. + /** @var WordPoints_PHPUnit_Mock_Hook_Extension $blocker */ + $blocker = $extensions->get( 'blocker' ); + + $blocker->should_hit = false; + + $settings = array( + $this->extension_slug => array( 'test_fire' => array( array( 'length' => DAY_IN_SECONDS ) ) ), + 'target' => array( 'test_entity' ), + ); + + $reaction = $this->factory->wordpoints->hook_reaction->create( $settings ); + + $this->assertIsReaction( $reaction ); + + $event_args = new WordPoints_Hook_Event_Args( + array( new WordPoints_Hook_Arg( 'test_entity' ) ) + ); + + $get_period_queries = new WordPoints_PHPUnit_Mock_Filter(); + $get_period_queries->count_callback = array( $this, 'is_get_period_query' ); + add_filter( 'query', array( $get_period_queries, 'filter' ) ); + + $get_by_reaction_queries = new WordPoints_PHPUnit_Mock_Filter(); + $get_by_reaction_queries->count_callback = array( $this, 'is_get_period_by_reaction_query' ); + add_filter( 'query', array( $get_by_reaction_queries, 'filter' ) ); + + // Run once, to show normal behavior. + $hooks->fire( 'test_event', $event_args, 'test_fire' ); + + $this->assertSame( 0, $get_period_queries->call_count ); + $this->assertSame( 1, $get_by_reaction_queries->call_count ); + + $test_reactor = $hooks->get_sub_app( 'reactors' )->get( 'test_reactor' ); + + $this->assertCount( 0, $test_reactor->hits ); + + // Flush the cache and run again, to demonstrate un-cached behavior. + $this->flush_cache(); + + $hooks->fire( 'test_event', $event_args, 'test_fire' ); + + $this->assertSame( 0, $get_period_queries->call_count ); + $this->assertSame( 2, $get_by_reaction_queries->call_count ); + + $this->assertCount( 0, $test_reactor->hits ); + + // Now run again, to show the cache's effect. + $hooks->fire( 'test_event', $event_args, 'test_fire' ); + + $this->assertSame( 0, $get_period_queries->call_count ); + $this->assertSame( 2, $get_by_reaction_queries->call_count ); + + $this->assertCount( 0, $test_reactor->hits ); + + // Turn the blocker off, to show what happens when a hit eventually occurs. + $blocker->should_hit = true; + + $hooks->fire( 'test_event', $event_args, 'test_fire' ); + + $this->assertSame( 0, $get_period_queries->call_count ); + $this->assertSame( 2, $get_by_reaction_queries->call_count ); + + $this->assertCount( 1, $test_reactor->hits ); + + // Finally, run again, to show that the cache was updated correctly. + $hooks->fire( 'test_event', $event_args, 'test_fire' ); + + $this->assertSame( 1, $get_period_queries->call_count ); + $this->assertSame( 2, $get_by_reaction_queries->call_count ); + + $this->assertCount( 1, $test_reactor->hits ); + } + /** * Travel forward in time by modifying the hit time of a period. * From 203eb18ad32b700c17a43f39ce44e3e0478b0285 Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Mon, 30 Apr 2018 13:49:55 -0500 Subject: [PATCH 3/5] hooks: let reactors validate a target before firing A reactor is not always able to react to a target if it does not meet certain requirements. For example, if the current user is actually a guest and not a logged in user, the Points reactor cannot award points. By letting the reactor validate the target, we are improving performance by preventing an unnecessary fire from being processed by the extensions, as well as ensuring that unusable data is not added to the database. In particular, this will improve performance of the Visit event, by avoiding calling the Periods extension into play. This is especially important because the Visit event can be triggered on every single page-load. See #762, #640 --- dev-lib | 2 +- src/classes/hook/extension/conditions.php | 1 + src/classes/hook/reaction/validator.php | 1 + .../hook/reactor/target/validatori.php | 34 ++++++ src/classes/hooks.php | 11 ++ src/classes/index.php | 1 + .../points/classes/hook/reactor.php | 23 +++- tests/phpunit/tests/classes/hooks.php | 108 ++++++++++++++++++ .../tests/points/classes/hook/reactor.php | 74 ++++++++++++ 9 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/classes/hook/reactor/target/validatori.php diff --git a/dev-lib b/dev-lib index 39ca95ba..84643175 160000 --- a/dev-lib +++ b/dev-lib @@ -1 +1 @@ -Subproject commit 39ca95ba3c922dc0f17704fc0043e0f0203daa81 +Subproject commit 846431755593ea36879426d2e68bef10b27f3fa7 diff --git a/src/classes/hook/extension/conditions.php b/src/classes/hook/extension/conditions.php index 66041b9f..176bd7b3 100644 --- a/src/classes/hook/extension/conditions.php +++ b/src/classes/hook/extension/conditions.php @@ -175,6 +175,7 @@ protected function validate_condition( $settings ) { return false; } + /** @var WordPoints_Hook_ConditionI $condition */ $condition = $this->conditions->get( $data_type, $settings['type'] ); if ( ! $condition ) { diff --git a/src/classes/hook/reaction/validator.php b/src/classes/hook/reaction/validator.php index 5a0c489a..2f99f65c 100644 --- a/src/classes/hook/reaction/validator.php +++ b/src/classes/hook/reaction/validator.php @@ -174,6 +174,7 @@ public function validate() { $this->event_args = new WordPoints_Hook_Event_Args( $event_args ); $this->event_args->set_validator( $this ); + /** @var WordPoints_Hook_ReactorI $reactor */ $reactor = $reactors->get( $this->reactor_slug ); $this->settings = $reactor->validate_settings( $this->settings, $this, $this->event_args ); diff --git a/src/classes/hook/reactor/target/validatori.php b/src/classes/hook/reactor/target/validatori.php new file mode 100644 index 00000000..287917e5 --- /dev/null +++ b/src/classes/hook/reactor/target/validatori.php @@ -0,0 +1,34 @@ +event_args->get_from_hierarchy( + $fire->reaction->get_meta( 'target' ) + ); + + if ( ! $reactor->can_hit( $target, $fire ) ) { + return; + } + } + /** @var WordPoints_Hook_ExtensionI[] $extensions */ $extensions = $this->get_sub_app( 'extensions' )->get_all(); diff --git a/src/classes/index.php b/src/classes/index.php index 6e38bac5..cbcbeaaa 100644 --- a/src/classes/index.php +++ b/src/classes/index.php @@ -19,6 +19,7 @@ 'wordpoints_hook_ui_script_data_provideri' => 'hook/ui/script/data/provideri.php', 'wordpoints_hook_settingsi' => 'hook/settingsi.php', 'wordpoints_hook_reactori' => 'hook/reactori.php', + 'wordpoints_hook_reactor_target_validatori' => 'hook/reactor/target/validatori.php', 'wordpoints_hook_reactioni' => 'hook/reactioni.php', 'wordpoints_hook_reaction_storei' => 'hook/reaction/storei.php', 'wordpoints_hook_extensioni' => 'hook/extensioni.php', diff --git a/src/components/points/classes/hook/reactor.php b/src/components/points/classes/hook/reactor.php index a4106a0f..943b2cb2 100644 --- a/src/components/points/classes/hook/reactor.php +++ b/src/components/points/classes/hook/reactor.php @@ -12,7 +12,9 @@ * * @since 2.1.0 */ -class WordPoints_Points_Hook_Reactor extends WordPoints_Hook_Reactor { +class WordPoints_Points_Hook_Reactor + extends WordPoints_Hook_Reactor + implements WordPoints_Hook_Reactor_Target_ValidatorI { /** * @since 2.1.0 @@ -122,6 +124,25 @@ public function update_settings( $reaction->update_meta( 'log_text', $settings['log_text'] ); } + /** + * @since 2.4.2 + */ + public function can_hit( + WordPoints_EntityishI $target, + WordPoints_Hook_Fire $fire + ) { + + if ( 'toggle_off' === $fire->action_type ) { + return true; + } + + if ( ! $target instanceof WordPoints_Entity || ! $target->get_the_id() ) { + return false; + } + + return true; + } + /** * @since 2.1.0 */ diff --git a/tests/phpunit/tests/classes/hooks.php b/tests/phpunit/tests/classes/hooks.php index 90054727..fe39088d 100644 --- a/tests/phpunit/tests/classes/hooks.php +++ b/tests/phpunit/tests/classes/hooks.php @@ -564,6 +564,114 @@ public function test_fire_event_invalid_reaction() { ); } + /** + * Test firing an event when the reactor must validate the target. + * + * @since 2.4.2 + */ + public function test_fire_event_target_validator() { + + /** @var WordPoints_PHPUnit_Mock_Hook_Extension $extension */ + $extension = $this->factory->wordpoints->hook_extension->create_and_get(); + + /** @var WordPoints_PHPUnit_Mock_Hook_Extension $other_extension */ + $other_extension = $this->factory->wordpoints->hook_extension->create_and_get( + array( 'slug' => 'another' ) + ); + + /** @var WordPoints_PHPUnit_Mock_Hook_Reactor_Target_Validator $reactor */ + $reactor = $this->factory->wordpoints->hook_reactor->create_and_get( + array( 'class' => 'WordPoints_PHPUnit_Mock_Hook_Reactor_Target_Validator' ) + ); + + $reactions = $this->factory->wordpoints->hook_reaction->create_many( 2 ); + + /** @var WordPoints_PHPUnit_Mock_Hook_Reactor $other_reactor */ + $other_reactor = $this->factory->wordpoints->hook_reactor->create_and_get( + array( 'slug' => 'another' ) + ); + + $other_reaction = $this->factory->wordpoints->hook_reaction->create( + array( 'reactor' => 'another' ) + ); + + $this->fire_event(); + + // The extensions should have each been checked. + $this->assertCount( 3, $extension->hit_checks ); + $this->assertCount( 3, $extension->hits ); + + $this->assertCount( 3, $other_extension->hit_checks ); + $this->assertCount( 3, $other_extension->hits ); + + // The reactors should have been hit. + $this->assertCount( 2, $reactor->hits ); + + $this->assertHitsLogged( array( 'reaction_id' => $reactions[0]->get_id() ) ); + $this->assertHitsLogged( array( 'reaction_id' => $reactions[1]->get_id() ) ); + + $this->assertCount( 1, $other_reactor->hits ); + + $this->assertHitsLogged( + array( 'reactor' => 'another', 'reaction_id' => $other_reaction->get_id() ) + ); + } + + /** + * Test firing an event when the reactor cannot hit the target. + * + * @since 2.4.2 + */ + public function test_fire_event_target_invalid() { + + /** @var WordPoints_PHPUnit_Mock_Hook_Extension $extension */ + $extension = $this->factory->wordpoints->hook_extension->create_and_get(); + + /** @var WordPoints_PHPUnit_Mock_Hook_Extension $other_extension */ + $other_extension = $this->factory->wordpoints->hook_extension->create_and_get( + array( 'slug' => 'another' ) + ); + + /** @var WordPoints_PHPUnit_Mock_Hook_Reactor_Target_Validator $reactor */ + $reactor = $this->factory->wordpoints->hook_reactor->create_and_get( + array( 'class' => 'WordPoints_PHPUnit_Mock_Hook_Reactor_Target_Validator' ) + ); + + $reactor->target_validated = false; + + $reactions = $this->factory->wordpoints->hook_reaction->create_many( 2 ); + + /** @var WordPoints_PHPUnit_Mock_Hook_Reactor $other_reactor */ + $other_reactor = $this->factory->wordpoints->hook_reactor->create_and_get( + array( 'slug' => 'another' ) + ); + + $other_reaction = $this->factory->wordpoints->hook_reaction->create( + array( 'reactor' => 'another' ) + ); + + $this->fire_event(); + + // The extensions should have each been checked. + $this->assertCount( 1, $extension->hit_checks ); + $this->assertCount( 1, $extension->hits ); + + $this->assertCount( 1, $other_extension->hit_checks ); + $this->assertCount( 1, $other_extension->hits ); + + // The first reactor should not have been hit. + $this->assertCount( 0, $reactor->hits ); + + $this->assertHitsLogged( array( 'reaction_id' => $reactions[0]->get_id() ), 0 ); + $this->assertHitsLogged( array( 'reaction_id' => $reactions[1]->get_id() ), 0 ); + + $this->assertCount( 1, $other_reactor->hits ); + + $this->assertHitsLogged( + array( 'reactor' => 'another', 'reaction_id' => $other_reaction->get_id() ) + ); + } + /** * Test firing an event that an extension aborts. * diff --git a/tests/phpunit/tests/points/classes/hook/reactor.php b/tests/phpunit/tests/points/classes/hook/reactor.php index bbd9c2c7..63023697 100644 --- a/tests/phpunit/tests/points/classes/hook/reactor.php +++ b/tests/phpunit/tests/points/classes/hook/reactor.php @@ -247,6 +247,80 @@ public function test_update_settings() { $this->assertSame( $settings['log_text'], $reaction->get_meta( 'log_text' ) ); } + /** + * Test checking if the target can be hit. + * + * @since 2.4.2 + */ + public function test_can_hit() { + + $event_args = new WordPoints_Hook_Event_Args( array() ); + + /** @var WordPoints_Entity_User $entity */ + $entity = wordpoints_entities()->get( 'user' ); + + $user_id = $this->factory->user->create(); + + $entity->set_the_value( $user_id ); + + $event_args->add_entity( $entity ); + + $reaction = $this->factory->wordpoints->hook_reaction->create(); + + $fire = new WordPoints_Hook_Fire( $event_args, $reaction, 'test_fire' ); + $fire->hit(); + + $this->assertTrue( $this->reactor->can_hit( $entity, $fire ) ); + } + + /** + * Test checking if the target can be hit when a reversal action is occurring. + * + * @since 2.4.2 + */ + public function test_can_hit_reversal() { + + $event_args = new WordPoints_Hook_Event_Args( array() ); + + /** @var WordPoints_Entity_User $entity */ + $entity = wordpoints_entities()->get( 'user' ); + + $user_id = $this->factory->user->create(); + + $entity->set_the_value( $user_id ); + + $event_args->add_entity( $entity ); + + $reaction = $this->factory->wordpoints->hook_reaction->create(); + + $fire = new WordPoints_Hook_Fire( $event_args, $reaction, 'toggle_off' ); + $fire->hit(); + + $this->assertTrue( $this->reactor->can_hit( $entity, $fire ) ); + } + + /** + * Test checking if the target can be hit when the entity ID isn't set. + * + * @since 2.4.2 + */ + public function test_can_hit_value_not_set() { + + $event_args = new WordPoints_Hook_Event_Args( array() ); + + /** @var WordPoints_Entity_User $entity */ + $entity = wordpoints_entities()->get( 'user' ); + + $event_args->add_entity( $entity ); + + $reaction = $this->factory->wordpoints->hook_reaction->create(); + + $fire = new WordPoints_Hook_Fire( $event_args, $reaction, 'test_fire' ); + $fire->hit(); + + $this->assertFalse( $this->reactor->can_hit( $entity, $fire ) ); + } + /** * Test hitting the target. * From 1f8302ae1ff698e6097f587d1c81f5479cd7a844 Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Thu, 3 May 2018 13:50:11 -0500 Subject: [PATCH 4/5] hooks: fix target validation test These tests were failing because we weren't actually including the target in the event args. See #762 --- dev-lib | 2 +- tests/phpunit/tests/classes/hooks.php | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/dev-lib b/dev-lib index 84643175..e4f75d8e 160000 --- a/dev-lib +++ b/dev-lib @@ -1 +1 @@ -Subproject commit 846431755593ea36879426d2e68bef10b27f3fa7 +Subproject commit e4f75d8ea61d66c95123a30d78820fc376003d55 diff --git a/tests/phpunit/tests/classes/hooks.php b/tests/phpunit/tests/classes/hooks.php index fe39088d..def98cda 100644 --- a/tests/phpunit/tests/classes/hooks.php +++ b/tests/phpunit/tests/classes/hooks.php @@ -826,10 +826,30 @@ public function test_fire_event_with_miss_listener() { */ public function fire_event() { - $args = new WordPoints_Hook_Event_Args( array() ); + $args = new WordPoints_Hook_Event_Args( + array( + 'test_entity' => new WordPoints_PHPUnit_Mock_Hook_Arg( 'test_entity' ), + ) + ); wordpoints_hooks()->fire( 'test_event', $args, 'test_fire' ); } + + /** + * @since 2.4.2 + */ + public function assertHitsLogged( + array $data, + $count = 1, + $signature_arg_guids = null + ) { + + if ( null === $signature_arg_guids ) { + $signature_arg_guids = '{"test_entity":1,"site":1,"network":1}'; + } + + parent::assertHitsLogged( $data, $count, $signature_arg_guids ); + } } // EOF From dd7041fc6555f449ebd0c690ee53875faa4fe582 Mon Sep 17 00:00:00 2001 From: JDGrimes Date: Mon, 7 May 2018 13:49:11 -0500 Subject: [PATCH 5/5] phpunit: Update required version of WPPPB For compatibility with PHPUnit 6. --- composer.json | 2 +- composer.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 63c2d80d..d57b3af9 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ }, "require-dev": { "jdgrimes/wp-filesystem-mock": "^0.1.2", - "jdgrimes/wpppb": "^0.3.0", + "jdgrimes/wpppb": "^0.3.5", "xrstf/composer-php52": "^1.0", "jdgrimes/wp-http-testcase": "^1.3" }, diff --git a/composer.lock b/composer.lock index d6fbc389..0017f748 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "4b4df64c5fa45140c069b96dbfca89b9", + "content-hash": "2b0efe0a43f4b4fd1266037372d7ba9c", "packages": [], "packages-dev": [ { @@ -87,16 +87,16 @@ }, { "name": "jdgrimes/wpppb", - "version": "0.3.1", + "version": "0.3.5", "source": { "type": "git", "url": "https://github.com/JDGrimes/wpppb.git", - "reference": "a19834f29aa690653bcdb908a0a9476008021922" + "reference": "faaac99b5a187b700cdb3eb16f0989f4eee975ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JDGrimes/wpppb/zipball/a19834f29aa690653bcdb908a0a9476008021922", - "reference": "a19834f29aa690653bcdb908a0a9476008021922", + "url": "https://api.github.com/repos/JDGrimes/wpppb/zipball/faaac99b5a187b700cdb3eb16f0989f4eee975ce", + "reference": "faaac99b5a187b700cdb3eb16f0989f4eee975ce", "shasum": "" }, "require": { @@ -125,7 +125,7 @@ ], "description": "Bootstrap for integration testing WordPress plugins with PHPUnit", "homepage": "https://github.com/JDGrimes/wpppb", - "time": "2017-10-09T20:56:07+00:00" + "time": "2018-01-03T22:31:21+00:00" }, { "name": "xrstf/composer-php52",