diff --git a/composer.json b/composer.json index c4dc37696..89aeba973 100644 --- a/composer.json +++ b/composer.json @@ -45,6 +45,7 @@ "kriswallsmith/assetic": "^1.4", "laminas/laminas-permissions-acl": "^2.8", "league/climate": "^3.2", + "league/event": "^3.0", "league/flysystem": "^1.0", "mcaskill/php-html-build-attributes": "^1.0", "monolog/monolog": "^1.17", @@ -95,6 +96,7 @@ ], "Charcoal\\Admin\\": "packages/admin/src/Charcoal/Admin/", "Charcoal\\Email\\": "packages/email/src/Charcoal/Email", + "Charcoal\\Event\\": "packages/event/src/Charcoal/Event/", "Charcoal\\Object\\": "packages/object/src/Charcoal/Object", "Charcoal\\User\\": "packages/user/src/Charcoal/User", "Charcoal\\View\\": "packages/view/src/Charcoal/View" @@ -114,6 +116,7 @@ "packages/config/tests/Charcoal/", "packages/core/tests/Charcoal", "packages/email/tests/Charcoal", + "packages/event/tests/Charcoal", "packages/factory/tests/Charcoal/", "packages/image/tests/Charcoal", "packages/object/tests/Charcoal/", @@ -135,6 +138,7 @@ "charcoal/config": "self.version", "charcoal/core": "self.version", "charcoal/email": "self.version", + "charcoal/event": "3.1.8", "charcoal/factory": "self.version", "charcoal/image": "self.version", "charcoal/object": "self.version", diff --git a/composer.lock b/composer.lock index f098188f5..ddfc35471 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "604ff6da0b39eb152bc0a35f54757e8c", + "content-hash": "dfb33ddbf6308b7c1c3f715d971f9c6a", "packages": [ { "name": "barryvdh/elfinder-flysystem-driver", @@ -392,16 +392,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.4.1", + "version": "2.4.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379" + "reference": "67c26b443f348a51926030c83481b85718457d3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/69568e4293f4fa993f3b0e51c9723e1e17c41379", - "reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d", + "reference": "67c26b443f348a51926030c83481b85718457d3d", "shasum": "" }, "require": { @@ -491,7 +491,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.1" + "source": "https://github.com/guzzle/psr7/tree/2.4.3" }, "funding": [ { @@ -507,7 +507,7 @@ "type": "tidelift" } ], - "time": "2022-08-28T14:45:39+00:00" + "time": "2022-10-26T14:07:24+00:00" }, { "name": "intervention/image", @@ -802,18 +802,77 @@ }, "time": "2022-06-18T14:42:08+00:00" }, + { + "name": "league/event", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "221867a61087ee265ca07bd39aa757879afca820" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/221867a61087ee265ca07bd39aa757879afca820", + "reference": "221867a61087ee265ca07bd39aa757879afca820", + "shasum": "" + }, + "require": { + "php": ">=7.2.0", + "psr/event-dispatcher": "^1.0" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.45", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/3.0.2" + }, + "time": "2022-10-29T09:31:25+00:00" + }, { "name": "league/flysystem", - "version": "1.1.9", + "version": "1.1.10", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "094defdb4a7001845300334e7c1ee2335925ef99" + "reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/094defdb4a7001845300334e7c1ee2335925ef99", - "reference": "094defdb4a7001845300334e7c1ee2335925ef99", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3239285c825c152bcc315fe0e87d6b55f5972ed1", + "reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1", "shasum": "" }, "require": { @@ -886,7 +945,7 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/1.1.9" + "source": "https://github.com/thephpleague/flysystem/tree/1.1.10" }, "funding": [ { @@ -894,7 +953,7 @@ "type": "other" } ], - "time": "2021-12-09T09:40:50+00:00" + "time": "2022-10-04T09:16:37+00:00" }, { "name": "league/flysystem-cached-adapter", @@ -1193,16 +1252,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.6.4", + "version": "v6.6.5", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b" + "reference": "8b6386d7417526d1ea4da9edb70b8352f7543627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/a94fdebaea6bd17f51be0c2373ab80d3d681269b", - "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/8b6386d7417526d1ea4da9edb70b8352f7543627", + "reference": "8b6386d7417526d1ea4da9edb70b8352f7543627", "shasum": "" }, "require": { @@ -1226,8 +1285,8 @@ "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", "league/oauth2-google": "Needed for Google XOAUTH2 authentication", "psr/log": "For optional PSR-3 debug logging", - "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", - "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" }, "type": "library", "autoload": { @@ -1259,7 +1318,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.4" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.5" }, "funding": [ { @@ -1267,7 +1326,7 @@ "type": "github" } ], - "time": "2022-08-22T09:22:00+00:00" + "time": "2022-10-07T12:23:10+00:00" }, { "name": "phpoption/phpoption", @@ -1494,6 +1553,56 @@ }, "time": "2021-11-05T16:50:12+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/http-client", "version": "1.0.1", @@ -2091,16 +2200,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", - "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", "shasum": "" }, "require": { @@ -2115,7 +2224,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2153,7 +2262,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" }, "funding": [ { @@ -2169,20 +2278,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", - "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", "shasum": "" }, "require": { @@ -2197,7 +2306,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2236,7 +2345,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" }, "funding": [ { @@ -2252,20 +2361,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", - "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", "shasum": "" }, "require": { @@ -2274,7 +2383,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2319,7 +2428,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" }, "funding": [ { @@ -2335,7 +2444,7 @@ "type": "tidelift" } ], - "time": "2022-05-10T07:21:04+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/process", @@ -2558,16 +2667,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.4.1", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "264dce589e7ce37a7ba99cb901eed8249fbec92f" + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/264dce589e7ce37a7ba99cb901eed8249fbec92f", - "reference": "264dce589e7ce37a7ba99cb901eed8249fbec92f", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", "shasum": "" }, "require": { @@ -2582,15 +2691,19 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.4.1", "ext-filter": "*", - "phpunit/phpunit": "^7.5.20 || ^8.5.21 || ^9.5.10" + "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" }, "suggest": { "ext-filter": "Required to use the boolean validator." }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, "branch-alias": { - "dev-master": "5.4-dev" + "dev-master": "5.5-dev" } }, "autoload": { @@ -2622,7 +2735,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.4.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" }, "funding": [ { @@ -2634,7 +2747,7 @@ "type": "tidelift" } ], - "time": "2021-12-12T23:22:04+00:00" + "time": "2022-10-16T01:01:54+00:00" } ], "packages-dev": [ @@ -2690,16 +2803,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.235.8", + "version": "3.242.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "cc33d53d735a3835adff212598f2a20ee9ac9531" + "reference": "9bfd85f696fff6a9b7810f1361751ad33a9b23d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cc33d53d735a3835adff212598f2a20ee9ac9531", - "reference": "cc33d53d735a3835adff212598f2a20ee9ac9531", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9bfd85f696fff6a9b7810f1361751ad33a9b23d1", + "reference": "9bfd85f696fff6a9b7810f1361751ad33a9b23d1", "shasum": "" }, "require": { @@ -2778,9 +2891,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.235.8" + "source": "https://github.com/aws/aws-sdk-php/tree/3.242.1" }, - "time": "2022-09-14T18:18:31+00:00" + "time": "2022-11-11T19:59:24+00:00" }, { "name": "cache/adapter-common", @@ -3922,16 +4035,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.38", + "version": "2.0.39", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "b03536539f43a4f9aa33c4f0b2f3a1c752088fcd" + "reference": "f3a0e2b715c40cf1fd270d444901b63311725d63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/b03536539f43a4f9aa33c4f0b2f3a1c752088fcd", - "reference": "b03536539f43a4f9aa33c4f0b2f3a1c752088fcd", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f3a0e2b715c40cf1fd270d444901b63311725d63", + "reference": "f3a0e2b715c40cf1fd270d444901b63311725d63", "shasum": "" }, "require": { @@ -4012,7 +4125,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/2.0.38" + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.39" }, "funding": [ { @@ -4028,20 +4141,20 @@ "type": "tidelift" } ], - "time": "2022-09-02T17:04:26+00:00" + "time": "2022-10-24T10:49:03+00:00" }, { "name": "phpstan/phpstan", - "version": "1.8.5", + "version": "1.9.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20" + "reference": "d6fdf01c53978b6429f1393ba4afeca39cc68afa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6598a5ff12ca4499a836815e08b4d77a2ddeb20", - "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d6fdf01c53978b6429f1393ba4afeca39cc68afa", + "reference": "d6fdf01c53978b6429f1393ba4afeca39cc68afa", "shasum": "" }, "require": { @@ -4071,7 +4184,7 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.5" + "source": "https://github.com/phpstan/phpstan/tree/1.9.2" }, "funding": [ { @@ -4087,20 +4200,20 @@ "type": "tidelift" } ], - "time": "2022-09-07T16:05:32+00:00" + "time": "2022-11-10T09:56:11+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.17", + "version": "9.2.18", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" + "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", - "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/12fddc491826940cf9b7e88ad9664cf51f0f6d0a", + "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a", "shasum": "" }, "require": { @@ -4156,7 +4269,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.18" }, "funding": [ { @@ -4164,7 +4277,7 @@ "type": "github" } ], - "time": "2022-08-30T12:24:04+00:00" + "time": "2022-10-27T13:35:33+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4409,16 +4522,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.24", + "version": "9.5.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5" + "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/851867efcbb6a1b992ec515c71cdcf20d895e9d2", + "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2", "shasum": "" }, "require": { @@ -4440,14 +4553,14 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.1", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, "suggest": { @@ -4491,7 +4604,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.26" }, "funding": [ { @@ -4501,9 +4614,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-08-30T07:42:16+00:00" + "time": "2022-10-28T06:00:21+00:00" }, { "name": "psr/simple-cache", @@ -5657,16 +5774,16 @@ }, { "name": "symfony/console", - "version": "v4.4.45", + "version": "v4.4.48", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "28b77970939500fb04180166a1f716e75a871ef8" + "reference": "8e70c1cab07ac641b885ce80385b9824a293c623" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/28b77970939500fb04180166a1f716e75a871ef8", - "reference": "28b77970939500fb04180166a1f716e75a871ef8", + "url": "https://api.github.com/repos/symfony/console/zipball/8e70c1cab07ac641b885ce80385b9824a293c623", + "reference": "8e70c1cab07ac641b885ce80385b9824a293c623", "shasum": "" }, "require": { @@ -5727,7 +5844,7 @@ "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.45" + "source": "https://github.com/symfony/console/tree/v4.4.48" }, "funding": [ { @@ -5743,20 +5860,20 @@ "type": "tidelift" } ], - "time": "2022-08-17T14:50:19+00:00" + "time": "2022-10-26T16:02:45+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.12", + "version": "v5.4.13", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447" + "reference": "ac09569844a9109a5966b9438fc29113ce77cf51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/2d67c1f9a1937406a9be3171b4b22250c0a11447", - "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ac09569844a9109a5966b9438fc29113ce77cf51", + "reference": "ac09569844a9109a5966b9438fc29113ce77cf51", "shasum": "" }, "require": { @@ -5791,7 +5908,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.12" + "source": "https://github.com/symfony/filesystem/tree/v5.4.13" }, "funding": [ { @@ -5807,20 +5924,20 @@ "type": "tidelift" } ], - "time": "2022-08-02T13:48:16+00:00" + "time": "2022-09-21T19:53:16+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", - "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", "shasum": "" }, "require": { @@ -5829,7 +5946,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5870,7 +5987,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" }, "funding": [ { @@ -5886,20 +6003,20 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.26.0", + "version": "v1.27.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", - "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a", + "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a", "shasum": "" }, "require": { @@ -5908,7 +6025,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.26-dev" + "dev-main": "1.27-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5949,7 +6066,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0" }, "funding": [ { @@ -5965,7 +6082,7 @@ "type": "tidelift" } ], - "time": "2022-05-24T11:49:31+00:00" + "time": "2022-11-03T14:55:06+00:00" }, { "name": "symfony/service-contracts", @@ -6052,16 +6169,16 @@ }, { "name": "symfony/stopwatch", - "version": "v5.4.5", + "version": "v5.4.13", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30" + "reference": "6df7a3effde34d81717bbef4591e5ffe32226d69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", - "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/6df7a3effde34d81717bbef4591e5ffe32226d69", + "reference": "6df7a3effde34d81717bbef4591e5ffe32226d69", "shasum": "" }, "require": { @@ -6094,7 +6211,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.5" + "source": "https://github.com/symfony/stopwatch/tree/v5.4.13" }, "funding": [ { @@ -6110,7 +6227,7 @@ "type": "tidelift" } ], - "time": "2022-02-18T16:06:09+00:00" + "time": "2022-09-28T13:19:49+00:00" }, { "name": "symfony/yaml", @@ -6281,16 +6398,16 @@ }, { "name": "twig/twig", - "version": "v3.4.2", + "version": "v3.4.3", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077" + "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", - "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", + "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", "shasum": "" }, "require": { @@ -6341,7 +6458,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.4.2" + "source": "https://github.com/twigphp/Twig/tree/v3.4.3" }, "funding": [ { @@ -6353,7 +6470,7 @@ "type": "tidelift" } ], - "time": "2022-08-12T06:47:24+00:00" + "time": "2022-09-28T08:42:51+00:00" } ], "aliases": [], diff --git a/packages/admin/composer.json b/packages/admin/composer.json index 32055adec..873c473d9 100644 --- a/packages/admin/composer.json +++ b/packages/admin/composer.json @@ -34,6 +34,7 @@ "charcoal/cms": "^4.0.1", "charcoal/core": "^4.0.1", "charcoal/email": "^4.0.1", + "charcoal/event": "^3.2", "charcoal/object": "^4.0.1", "charcoal/translator": "^4.0.1", "charcoal/ui": "^4.0.1", diff --git a/packages/admin/config/admin.config.default.json b/packages/admin/config/admin.config.default.json index 1cb5d3dfa..adb8c02fa 100644 --- a/packages/admin/config/admin.config.default.json +++ b/packages/admin/config/admin.config.default.json @@ -239,6 +239,9 @@ }, "tools/resize-images": { "ident": "charcoal/admin/script/tools/resize-images" + }, + "translation/parse": { + "ident": "charcoal/translator/script/translation-parser" } } }, diff --git a/packages/admin/src/Charcoal/Admin/Action/ElfinderConnectorAction.php b/packages/admin/src/Charcoal/Admin/Action/ElfinderConnectorAction.php index a1b37ca7d..bf79dcb66 100644 --- a/packages/admin/src/Charcoal/Admin/Action/ElfinderConnectorAction.php +++ b/packages/admin/src/Charcoal/Admin/Action/ElfinderConnectorAction.php @@ -2,6 +2,9 @@ namespace Charcoal\Admin\Action; +use Charcoal\Event\EventDispatcherTrait; +use Charcoal\Event\Events\FileWasUploaded; +use finfo; use InvalidArgumentException; use RuntimeException; use UnexpectedValueException; @@ -33,6 +36,7 @@ class ElfinderConnectorAction extends AdminAction { use CallableResolverAwareTrait; + use EventDispatcherTrait; /** * The default relative path (from filesystem's root) to the storage directory. @@ -186,6 +190,14 @@ public function setupElfinder(array $extraOptions = []) define('ELFINDER_IMG_PARENT_URL', (string)$this->baseUrl(ElfinderTemplate::ELFINDER_ASSETS_REL_PATH)); } + $extraOptions = array_merge($extraOptions, [ + 'bind' => [ + 'upload.presave' => [ + ':dispatchEventOnUploadPreSave', + ], + ], + ]); + $options = $this->buildConnectorOptions($extraOptions); // Run elFinder @@ -668,6 +680,27 @@ protected function translateFilesystemName(string $ident): ?string return null; } + /** + * Dispatches an event on `upload.presave`. + * + * @param string $path The target path. + * @param string $name The target name. + * @param string $src The temporary file name. + * @param object $elfinder The elFinder instance. + * @param object $volume The current volume instance. + * @return void|bool|array + */ + public function dispatchEventOnUploadPreSave(&$path, &$name, $src, $elfinder, $volume) + { + if (!$src || !file_exists($src)) { + return false; + } + + $this->getEventDispatcher()->dispatch(new FileWasUploaded($src)); + + return true; + } + /** * Sanitizes a file name on `upload.presave`. * @@ -897,6 +930,8 @@ public function setDependencies(Container $container) /** @see \Charcoal\App\ServiceProvide\FilesystemServiceProvider */ $this->filesystemConfig = $container['filesystem/config']; $this->filesystems = $container['filesystems']; + + $this->setEventDispatcher($container['app/event/dispatcher']); } /** diff --git a/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php b/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php index 3b320b310..5bb035f48 100644 --- a/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php +++ b/packages/admin/src/Charcoal/Admin/Action/Object/RevertRevisionAction.php @@ -2,14 +2,15 @@ namespace Charcoal\Admin\Action\Object; +use Charcoal\Object\RevisionsManager; use Exception; use InvalidArgumentException; // From PSR-7 +use Pimple\Container; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; // From 'charcoal-object' use Charcoal\Object\ObjectRevisionInterface; -use Charcoal\Object\RevisionableInterface; // From 'charcoal-admin' use Charcoal\Admin\AdminAction; use Charcoal\Admin\Ui\ObjectContainerInterface; @@ -45,6 +46,15 @@ class RevertRevisionAction extends AdminAction implements ObjectContainerInterfa */ protected $revNum; + private RevisionsManager $revisionManager; + + protected function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionManager = $container->get('revisions/manager'); + } + /** * Retrieve the list of parameters to extract from the HTTP request. * @@ -62,9 +72,9 @@ protected function validDataFromRequest() /** * Set the revision number to restore. * - * @param integer $revNum The revision number to load. - * @throws InvalidArgumentException If the given revision is invalid. + * @param integer $revNum The revision number to load. * @return ObjectContainerInterface Chainable + * @throws InvalidArgumentException If the given revision is invalid. */ protected function setRevNum($revNum) { @@ -91,8 +101,8 @@ public function revNum() } /** - * @param RequestInterface $request A PSR-7 compatible Request instance. - * @param ResponseInterface $response A PSR-7 compatible Response instance. + * @param RequestInterface $request A PSR-7 compatible Request instance. + * @param ResponseInterface $response A PSR-7 compatible Response instance. * @return ResponseInterface */ public function run(RequestInterface $request, ResponseInterface $response) @@ -107,19 +117,11 @@ public function run(RequestInterface $request, ResponseInterface $response) '{{ errorMessage }}' => $failMessage ]); - $obj = $this->obj(); - if (!($obj instanceof RevisionableInterface)) { - $this->setSuccess(false); - - $this->addFeedback('error', strtr('{{ model }} does not support revisions', [ - '{{ model }}' => $this->getSingularLabelFromObj($obj), - ])); - - return $response->withStatus(400); - } - + $obj = $this->obj(); $revNum = $this->revNum(); - $revision = $obj->revisionNum($revNum); + $this->revisionManager->setModel($obj); + + $revision = $this->revisionManager->getRevisionFromNumber($revNum); if (!$revision['id']) { $this->setSuccess(false); @@ -139,7 +141,7 @@ public function run(RequestInterface $request, ResponseInterface $response) return $response->withStatus(404); } - $result = $obj->revertToRevision($revNum); + $result = $this->revisionManager->revertToRevision($revNum); if ($result) { $doneMessage = $translator->translate( diff --git a/packages/admin/src/Charcoal/Admin/Config.php b/packages/admin/src/Charcoal/Admin/Config.php index 0f750216a..2c25ee540 100644 --- a/packages/admin/src/Charcoal/Admin/Config.php +++ b/packages/admin/src/Charcoal/Admin/Config.php @@ -50,10 +50,9 @@ class Config extends AbstractConfig * * @return array */ - public function defaults() + public function defaults(): array { - $baseDir = rtrim(realpath(__DIR__ . '/../../../'), '/'); - $confDir = $baseDir . '/config'; + $confDir = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'config'; return $this->loadFile($confDir . '/admin.config.default.json'); } diff --git a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php index 846efab13..cb5262de4 100644 --- a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php +++ b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsInterface.php @@ -12,5 +12,5 @@ interface ObjectRevisionsInterface /** * @return \Charcoal\Object\ObjectRevisionInterface[] */ - public function objectRevisions(); + public function objectRevisions(): array; } diff --git a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php index f30af4628..f03edf9fe 100644 --- a/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php +++ b/packages/admin/src/Charcoal/Admin/Ui/ObjectRevisionsTrait.php @@ -6,6 +6,7 @@ use Charcoal\Factory\FactoryInterface; // From 'charcoal-object' use Charcoal\Object\ObjectRevisionInterface; +use Charcoal\Object\RevisionsManager; /** * An implementation, as Trait, of the {@see \Charcoal\Admin\Ui\ObjectRevisionsInterface}. @@ -15,7 +16,7 @@ trait ObjectRevisionsTrait /** * @return ObjectRevisionInterface[] */ - public function objectRevisions() + public function objectRevisions(): array { if (!$this->objType() || !$this->objId()) { return []; @@ -24,7 +25,9 @@ public function objectRevisions() $obj = $this->modelFactory()->create($this->objType()); $obj->setId($this->objId()); - $lastRevision = $obj->latestRevision(); + $this->revisionManager()->setModel($obj); + + $lastRevision = $this->revisionManager()->getLatestRevision(); $propLabel = '%2$s'; $callback = function (ObjectRevisionInterface &$revision) use ($lastRevision, $obj, $propLabel) { @@ -67,7 +70,7 @@ public function objectRevisions() $revision->allowRevert = ($lastRevision['revNum'] !== $revision['revNum']); }; - return $obj->allRevisions($callback); + return $this->revisionManager()->getAllRevisions($callback); } /** @@ -88,4 +91,6 @@ abstract public function objId(); * @return FactoryInterface */ abstract protected function modelFactory(); + + abstract protected function revisionManager(): RevisionsManager; } diff --git a/packages/admin/src/Charcoal/Admin/User/AuthTokenMetadata.php b/packages/admin/src/Charcoal/Admin/User/AuthTokenMetadata.php index 9874dfae5..36161df0c 100644 --- a/packages/admin/src/Charcoal/Admin/User/AuthTokenMetadata.php +++ b/packages/admin/src/Charcoal/Admin/User/AuthTokenMetadata.php @@ -13,7 +13,7 @@ class AuthTokenMetadata extends BaseAuthTokenMetadata /** * @return array */ - public function defaults() + public function defaults(): array { $parentDefaults = parent::defaults(); diff --git a/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php b/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php index 526d6d514..9c1309345 100644 --- a/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php +++ b/packages/admin/src/Charcoal/Admin/Widget/FormGroup/ObjectRevisionsFormGroup.php @@ -3,6 +3,7 @@ namespace Charcoal\Admin\Widget\FormGroup; // From 'pimple/pimple' +use Charcoal\Object\RevisionsManager; use Pimple\Container; // From 'charcoal-core' use Charcoal\Model\ModelFactoryTrait; @@ -37,6 +38,12 @@ class ObjectRevisionsFormGroup extends AbstractFormGroup implements */ public $widgetId; + private RevisionsManager $revisionManager; + + protected function revisionManager(): RevisionsManager + { + return $this->revisionManager; + } /** * @param string $widgetId The widget identifier. @@ -110,6 +117,7 @@ protected function setDependencies(Container $container) parent::setDependencies($container); $this->setModelFactory($container['model/factory']); + $this->revisionManager = $container['revisions/manager']; $this->objType = $container['request']->getParam('obj_type'); $this->objId = $container['request']->getParam('obj_id'); diff --git a/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php b/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php index d2189706b..02941c3c1 100644 --- a/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php +++ b/packages/admin/src/Charcoal/Admin/Widget/FormSidebarWidget.php @@ -2,7 +2,7 @@ namespace Charcoal\Admin\Widget; -use Charcoal\Object\RevisionableInterface; +use Charcoal\Object\RevisionsManager; use Charcoal\User\AuthAwareInterface; use InvalidArgumentException; // From Pimple @@ -163,6 +163,8 @@ class FormSidebarWidget extends AdminWidget implements */ private $requiredGlobalAclPermissions = []; + private ?RevisionsManager $revisionManager; + /** * @param array|ArrayInterface $data Class data. * @return FormSidebarWidget Chainable @@ -547,13 +549,15 @@ public function isObjRevisionable() $this->isObjRevisionable = false; } else { $obj = $this->form()->obj(); - if (!$obj->id()) { + if (!$obj->id() || $this->revisionManager === null) { $this->isObjRevisionable = false; return $this->isObjRevisionable; } - if ($obj instanceof RevisionableInterface && $obj['revisionEnabled']) { - $this->isObjRevisionable = !!count($obj->allRevisions()); + $this->revisionManager->setModel($obj); + + if ($this->revisionManager->isRevisionEnabled()) { + $this->isObjRevisionable = !!count($this->revisionManager->getAllRevisions()); } } } @@ -894,4 +898,11 @@ protected function isAssoc(array $array) return !!array_filter($array, 'is_string', ARRAY_FILTER_USE_KEY); } + + protected function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionManager = ($container['revisions/manager'] ?? null); + } } diff --git a/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php b/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php index cb482ded4..e8299def0 100644 --- a/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php +++ b/packages/admin/src/Charcoal/Admin/Widget/ObjectRevisionsWidget.php @@ -8,6 +8,8 @@ use Charcoal\Admin\AdminWidget; use Charcoal\Admin\Ui\ObjectRevisionsInterface; use Charcoal\Admin\Ui\ObjectRevisionsTrait; +use Charcoal\Object\RevisionsManager; +use Pimple\Container; /** * Class ObjectRevisionWidget @@ -27,6 +29,20 @@ class ObjectRevisionsWidget extends AdminWidget implements */ protected $objId; + private RevisionsManager $revisionManager; + + protected function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionManager = $container['revisions/manager']; + } + + protected function revisionManager(): RevisionsManager + { + return $this->revisionManager; + } + /** * @return boolean */ diff --git a/packages/admin/tests/Charcoal/Admin/Action/LoginActionTest.php b/packages/admin/tests/Charcoal/Admin/Action/LoginActionTest.php index 8c814c806..0e0449d25 100644 --- a/packages/admin/tests/Charcoal/Admin/Action/LoginActionTest.php +++ b/packages/admin/tests/Charcoal/Admin/Action/LoginActionTest.php @@ -2,6 +2,7 @@ namespace Charcoal\Tests\Admin\Action; +use Charcoal\App\Facade\Facade; use PDO; use ReflectionClass; diff --git a/packages/admin/tests/Charcoal/Admin/ContainerProvider.php b/packages/admin/tests/Charcoal/Admin/ContainerProvider.php index fea6cebe0..05c4dda44 100644 --- a/packages/admin/tests/Charcoal/Admin/ContainerProvider.php +++ b/packages/admin/tests/Charcoal/Admin/ContainerProvider.php @@ -2,6 +2,7 @@ namespace Charcoal\Tests\Admin; +use Charcoal\App\Facade\Facade; use PDO; // From Mockery @@ -28,6 +29,7 @@ use League\CLImate\Util\Output; use League\CLImate\Util\Reader\Stdin; use League\CLImate\Util\UtilFactory; +use League\Event\EventDispatcher; // From 'charcoal-factory' use Charcoal\Factory\GenericFactory as Factory; @@ -91,11 +93,15 @@ public function registerDebug(Container $container) */ public function registerBaseServices(Container $container) { + Facade::clearResolvedFacadeInstances(); + Facade::setFacadeResolver($container); + $this->registerDebug($container); $this->registerConfig($container); $this->registerDatabase($container); $this->registerLogger($container); $this->registerCache($container); + $this->registerEvent($container); } /** @@ -379,6 +385,18 @@ public function registerCache(Container $container) }; } + /** + * Setup event dispatcher. + * + * @param Container $container A DI container. + * @return void + */ + public function registerEvent(Container $container) + { + $container['event/dispatcher'] = new EventDispatcher(); + $container['app/event/dispatcher'] = $container['event/dispatcher']; + } + /** * @param Container $container A DI container. * @return void @@ -541,10 +559,14 @@ public function registerEmailFactory(Container $container) */ public function registerActionDependencies(Container $container) { + Facade::clearResolvedFacadeInstances(); + Facade::setFacadeResolver($container); + $this->registerDebug($container); $this->registerLogger($container); $this->registerDatabase($container); $this->registerCache($container); + $this->registerEvent($container); $this->registerAdminConfig($container); $this->registerBaseUrl($container); diff --git a/packages/admin/tests/Charcoal/Admin/Service/ExporterTest.php b/packages/admin/tests/Charcoal/Admin/Service/ExporterTest.php index 49551abe9..818a77803 100644 --- a/packages/admin/tests/Charcoal/Admin/Service/ExporterTest.php +++ b/packages/admin/tests/Charcoal/Admin/Service/ExporterTest.php @@ -1,6 +1,6 @@ basePath()); $dotenv->safeLoad(); + $this->bootstrap(); + // Setup routes $this->routeManager()->setupRoutes(); @@ -143,6 +154,13 @@ private function setup() $this->setupMiddlewares(); } + private function bootstrap() + { + foreach ($this->bootstrappers as $bootstrapper) { + (new $bootstrapper())->bootstrap($this); + } + } + /** * Retrieve (create, if necessary) the application's route manager. * diff --git a/packages/app/src/Charcoal/App/AppConfig.php b/packages/app/src/Charcoal/App/AppConfig.php index 8c7b8a809..0853462af 100644 --- a/packages/app/src/Charcoal/App/AppConfig.php +++ b/packages/app/src/Charcoal/App/AppConfig.php @@ -19,68 +19,51 @@ class AppConfig extends AbstractConfig { /** * The application's timezone. - * - * @var string|null */ - private $timezone; + private ?string $timezone; /** * The application's name. * * For internal usage. - * - * @var string|null */ - private $projectName; + private ?string $projectName; /** * The base URL (public) for the Charcoal installation. - * - * @var UriInterface|null */ - private $baseUrl; + private ?UriInterface $baseUrl = null; /** * The base path for the Charcoal installation. - * - * @var string|null */ - private $basePath; + private ?string $basePath = null; /** * The path to the public / web directory. * - * @var string|null */ - private $publicPath; + private ?string $publicPath = null; /** * The path to the cache directory. - * - * @var string|null */ - private $cachePath; + private ?string $cachePath = null; /** * The path to the logs directory. - * - * @var string|null */ - private $logsPath; + private ?string $logsPath = null; /** * Whether the debug mode is enabled (TRUE) or not (FALSE). - * - * @var boolean */ - private $devMode = false; + private bool $devMode = false; /** * The application's routes. - * - * @var array */ - private $routes = []; + private array $routes = []; /** * The application's dynamic routes. @@ -91,84 +74,63 @@ class AppConfig extends AbstractConfig /** * The application's HTTP middleware. - * - * @var array */ - private $middlewares = []; + private array $middlewares = []; /** * The application's handlers. - * - * @var array */ - private $handlers = []; + private array $handlers = []; /** * The application's modules. - * - * @var array */ - private $modules = []; + private array $modules = []; /** * The application's API credentials and service configsets. - * - * @var array */ - private $apis = []; + private array $apis = []; /** * The application's caching configset. - * - * @var array */ - private $cache; + private array $cache = []; /** * The application's logging configset. * * @var array */ - private $logger; + private array $logger = []; /** * The application's view/rendering configset. - * - * @var array */ - protected $view; + protected array $view = []; /** * The application's database configsets. - * - * @var array */ - private $databases = []; + private array $databases = []; /** * The application's default database configset. - * - * @var string */ - private $defaultDatabase; + private ?string $defaultDatabase; /** * The application's filesystem configset. - * - * @var array */ - private $filesystem; + private array $filesystem = []; /** * Default app-config values. * * @return array */ - public function defaults() + public function defaults(): array { - /** @var string $baseDir Presume that Charcoal App _is_ the application */ - $baseDir = rtrim(realpath(__DIR__ . '/../../../'), '/') . '/'; - return [ 'project_name' => '', 'timezone' => 'UTC', @@ -182,7 +144,7 @@ public function defaults() 'view' => [], 'databases' => [], 'default_database' => 'default', - 'dev_mode' => false + 'dev_mode' => false, ]; } @@ -209,7 +171,7 @@ public function resolveValue($value) 'app.public_path' => $this->publicPath(), 'app.cache_path' => $this->cachePath(), 'app.logs_path' => $this->logsPath(), - 'packages.path' => ($_ENV['PACKAGES_PATH'] ?? 'vendor/charcoal') + 'packages.path' => ($_ENV['PACKAGES_PATH'] ?? 'vendor/charcoal'), ]; if (is_string($value)) { @@ -251,7 +213,7 @@ public function resolveValue($value) * @param string $path The file to load and add. * @return self */ - public function addFile($path) + public function addFile(string $path): self { $path = $this->resolveValue($path); @@ -263,11 +225,11 @@ public function addFile($path) * * Resolves symlinks with realpath() and ensure trailing slash. * - * @param string $path The absolute path to the application's root directory. + * @param string|null $path The absolute path to the application's root directory. * @throws InvalidArgumentException If the argument is not a string. * @return self */ - public function setBasePath($path) + public function setBasePath(?string $path): self { if ($path === null) { throw new InvalidArgumentException( @@ -290,7 +252,7 @@ public function setBasePath($path) * * @return string|null The absolute path to the application's root directory. */ - public function basePath() + public function basePath(): ?string { return $this->basePath; } @@ -298,11 +260,11 @@ public function basePath() /** * Set the application's absolute path to the public web directory. * - * @param string $path The path to the application's public directory. + * @param string|null $path The path to the application's public directory. * @throws InvalidArgumentException If the argument is not a string. * @return self */ - public function setPublicPath($path) + public function setPublicPath(?string $path): self { if ($path === null) { $this->publicPath = null; @@ -324,7 +286,7 @@ public function setPublicPath($path) * * @return string The absolute path to the application's public directory. */ - public function publicPath() + public function publicPath(): string { if ($this->publicPath === null) { $this->publicPath = $this->basePath() . DIRECTORY_SEPARATOR . 'www'; @@ -336,11 +298,11 @@ public function publicPath() /** * Set the application's absolute path to the cache directory. * - * @param string $path The path to the application's cache directory. + * @param string|null $path The path to the application's cache directory. * @throws InvalidArgumentException If the argument is not a string. * @return self */ - public function setCachePath($path) + public function setCachePath(?string $path): self { if ($path === null) { $this->cachePath = null; @@ -362,7 +324,7 @@ public function setCachePath($path) * * @return string The absolute path to the application's cache directory. */ - public function cachePath() + public function cachePath(): string { if ($this->cachePath === null) { $this->cachePath = $this->basePath() . DIRECTORY_SEPARATOR . 'var' . DIRECTORY_SEPARATOR . 'cache'; @@ -374,11 +336,11 @@ public function cachePath() /** * Set the application's absolute path to the logs directory. * - * @param string $path The path to the application's logs directory. + * @param string|null $path The path to the application's logs directory. * @throws InvalidArgumentException If the argument is not a string. * @return self */ - public function setLogsPath($path) + public function setLogsPath(?string $path): self { if ($path === null) { $this->logsPath = null; @@ -400,7 +362,7 @@ public function setLogsPath($path) * * @return string The absolute path to the application's logs directory. */ - public function logsPath() + public function logsPath(): string { if ($this->logsPath === null) { $this->logsPath = $this->basePath() . DIRECTORY_SEPARATOR . 'var' . DIRECTORY_SEPARATOR . 'logs'; @@ -415,7 +377,7 @@ public function logsPath() * @param UriInterface|string $uri The base URI to the application's web directory. * @return self */ - public function setBaseUrl($uri) + public function setBaseUrl($uri): self { if (is_string($uri)) { $this->baseUrl = Uri::createFromString($uri); @@ -430,7 +392,7 @@ public function setBaseUrl($uri) * * @return UriInterface|null The base URI to the application's web directory. */ - public function baseUrl() + public function baseUrl(): ?UriInterface { return $this->baseUrl; } @@ -442,14 +404,8 @@ public function baseUrl() * @throws InvalidArgumentException If the argument is not a string. * @return self */ - public function setTimezone($timezone) + public function setTimezone(string $timezone): self { - if (!is_string($timezone)) { - throw new InvalidArgumentException( - 'Timezone must be a string.' - ); - } - $this->timezone = $timezone; return $this; } @@ -461,13 +417,9 @@ public function setTimezone($timezone) * * @return string */ - public function timezone() + public function timezone(): string { - if (isset($this->timezone)) { - return $this->timezone; - } else { - return 'UTC'; - } + return ($this->timezone ?? 'UTC'); } /** @@ -477,7 +429,7 @@ public function timezone() * @throws InvalidArgumentException If the project argument is not a string (or null). * @return self */ - public function setProjectName($projectName) + public function setProjectName(?string $projectName): self { if ($projectName === null) { $this->projectName = null; @@ -496,7 +448,7 @@ public function setProjectName($projectName) /** * @return string|null */ - public function projectName() + public function projectName(): ?string { if ($this->projectName === null) { $baseUrl = $this->baseUrl(); @@ -512,7 +464,7 @@ public function projectName() * @param boolean $devMode The "dev mode" flag. * @return self */ - public function setDevMode($devMode) + public function setDevMode(bool $devMode): self { $this->devMode = !!$devMode; return $this; @@ -521,7 +473,7 @@ public function setDevMode($devMode) /** * @return boolean */ - public function devMode() + public function devMode(): bool { return !!$this->devMode; } @@ -533,7 +485,7 @@ public function devMode() * @throws InvalidArgumentException If the argument is not a configset. * @return self */ - public function setView(array $view) + public function setView(array $view): self { $this->view = $view; return $this; @@ -544,7 +496,7 @@ public function setView(array $view) * * @return array */ - public function view() + public function view(): array { return $this->view; } @@ -555,7 +507,7 @@ public function view() * @param array $apis The API configuration structure to set. * @return self */ - public function setApis(array $apis) + public function setApis(array $apis): self { $this->apis = $apis; return $this; @@ -564,7 +516,7 @@ public function setApis(array $apis) /** * @return array */ - public function apis() + public function apis(): array { return $this->apis; } @@ -576,7 +528,7 @@ public function apis() * @param array $routes The route configuration structure to set. * @return self */ - public function setRoutes(array $routes) + public function setRoutes(array $routes): self { $this->routes = $routes; return $this; @@ -585,7 +537,7 @@ public function setRoutes(array $routes) /** * @return array */ - public function routes() + public function routes(): array { return $this->routes; } @@ -594,7 +546,7 @@ public function routes() * @param array|boolean $routables The routable configuration structure to set or FALSE to disable dynamic routing. * @return self */ - public function setRoutables($routables) + public function setRoutables($routables): self { if ($routables !== false) { if (!is_array($routables) || empty($routables)) { @@ -620,7 +572,7 @@ public function routables() * @param array $middlewares The middleware configuration structure to set. * @return self */ - public function setMiddlewares(array $middlewares) + public function setMiddlewares(array $middlewares): self { $this->middlewares = $middlewares; return $this; @@ -629,7 +581,7 @@ public function setMiddlewares(array $middlewares) /** * @return array */ - public function middlewares() + public function middlewares(): array { return $this->middlewares; } @@ -647,7 +599,7 @@ public function middlewares() * @param array $handlers The handlers configuration structure to set. * @return self */ - public function setHandlers(array $handlers) + public function setHandlers(array $handlers): self { $this->handlers = $handlers; return $this; @@ -656,7 +608,7 @@ public function setHandlers(array $handlers) /** * @return array */ - public function handlers() + public function handlers(): array { return $this->handlers; } @@ -667,7 +619,7 @@ public function handlers() * @param array $modules The module configuration structure to set. * @return self */ - public function setModules(array $modules) + public function setModules(array $modules): self { $this->modules = $modules; return $this; @@ -676,7 +628,7 @@ public function setModules(array $modules) /** * @return array */ - public function modules() + public function modules(): array { return $this->modules; } @@ -688,7 +640,7 @@ public function modules() * @throws InvalidArgumentException If the argument is not a configset. * @return self */ - public function setCache(array $cache) + public function setCache(array $cache): self { $this->cache = $cache; return $this; @@ -699,7 +651,7 @@ public function setCache(array $cache) * * @return array */ - public function cache() + public function cache(): array { return $this->cache; } @@ -711,7 +663,7 @@ public function cache() * @throws InvalidArgumentException If the argument is not a configset. * @return self */ - public function setLogger(array $logger) + public function setLogger(array $logger): self { $this->logger = $logger; return $this; @@ -722,16 +674,16 @@ public function setLogger(array $logger) * * @return array */ - public function logger() + public function logger(): array { return $this->logger; } /** - * @param array $databases The avaiable databases config. + * @param array $databases The available databases config. * @return self */ - public function setDatabases(array $databases) + public function setDatabases(array $databases): self { $this->databases = $databases; return $this; @@ -741,7 +693,7 @@ public function setDatabases(array $databases) * @throws Exception If trying to access this method and no databases were set. * @return array */ - public function databases() + public function databases(): array { if ($this->databases === null) { throw new Exception( @@ -757,13 +709,8 @@ public function databases() * @throws Exception If trying to access an invalid database. * @return array */ - public function databaseConfig($ident) + public function databaseConfig(string $ident): array { - if (!is_string($ident)) { - throw new InvalidArgumentException( - 'Invalid app config: default database must be a string.' - ); - } $databases = $this->databases(); if (!isset($databases[$ident])) { throw new Exception( @@ -778,13 +725,8 @@ public function databaseConfig($ident) * @throws InvalidArgumentException If the argument is not a string. * @return self */ - public function setDefaultDatabase($defaultDatabase) + public function setDefaultDatabase(string $defaultDatabase): self { - if (!is_string($defaultDatabase)) { - throw new InvalidArgumentException( - 'Invalid app config: Default database must be a string.' - ); - } $this->defaultDatabase = $defaultDatabase; return $this; } @@ -795,14 +737,8 @@ public function setDefaultDatabase($defaultDatabase) * @throws InvalidArgumentException If the arguments are invalid. * @return self */ - public function addDatabase($ident, array $config) + public function addDatabase(string $ident, array $config): self { - if (!is_string($ident)) { - throw new InvalidArgumentException( - 'Invalid app config: database ident must be a string.' - ); - } - if ($this->databases === null) { $this->databases = []; } @@ -812,9 +748,9 @@ public function addDatabase($ident, array $config) /** * @throws Exception If trying to access this method before a setter. - * @return mixed + * @return string */ - public function defaultDatabase() + public function defaultDatabase(): string { if ($this->defaultDatabase === null) { throw new Exception( @@ -831,7 +767,7 @@ public function defaultDatabase() * @throws InvalidArgumentException If the argument is not a configset. * @return self */ - public function setFilesystem(array $filesystem) + public function setFilesystem(array $filesystem): self { $this->filesystem = $filesystem; return $this; @@ -842,7 +778,7 @@ public function setFilesystem(array $filesystem) * * @return array */ - public function filesystem() + public function filesystem(): array { return $this->filesystem; } diff --git a/packages/app/src/Charcoal/App/Bootstrap/RegisterFacades.php b/packages/app/src/Charcoal/App/Bootstrap/RegisterFacades.php new file mode 100644 index 000000000..126c87a4b --- /dev/null +++ b/packages/app/src/Charcoal/App/Bootstrap/RegisterFacades.php @@ -0,0 +1,18 @@ +getContainer()); + } +} diff --git a/packages/app/src/Charcoal/App/Config/DatabaseConfig.php b/packages/app/src/Charcoal/App/Config/DatabaseConfig.php index c5533aa49..a76296eb3 100644 --- a/packages/app/src/Charcoal/App/Config/DatabaseConfig.php +++ b/packages/app/src/Charcoal/App/Config/DatabaseConfig.php @@ -44,7 +44,7 @@ class DatabaseConfig extends AbstractConfig /** * @return array */ - public function defaults() + public function defaults(): array { return [ 'type' => 'mysql', diff --git a/packages/app/src/Charcoal/App/Config/LoggerConfig.php b/packages/app/src/Charcoal/App/Config/LoggerConfig.php index 800314189..214056ff1 100644 --- a/packages/app/src/Charcoal/App/Config/LoggerConfig.php +++ b/packages/app/src/Charcoal/App/Config/LoggerConfig.php @@ -50,7 +50,7 @@ class LoggerConfig extends AbstractConfig * * @return array */ - public function defaults() + public function defaults(): array { return [ 'active' => true, diff --git a/packages/app/src/Charcoal/App/Facade/Event.php b/packages/app/src/Charcoal/App/Facade/Event.php new file mode 100644 index 000000000..475140e5f --- /dev/null +++ b/packages/app/src/Charcoal/App/Facade/Event.php @@ -0,0 +1,30 @@ + + */ + protected static array $resolvedInstances = []; + + public static function setFacadeResolver(Container $resolver): void + { + static::$resolver = $resolver; + } + + /** + * @return object + */ + public static function getFacadeInstance(): object + { + return static::resolveFacadeInstance(static::getFacadeName()); + } + + /** + * Get the container service key the facade is providing alias for. + */ + protected static function getFacadeName(): string + { + throw new RuntimeException(sprintf( + 'The facade [%s] does not provide a container service key.', + get_called_class() + )); + } + + protected static function resolveFacadeInstance(string $key): object + { + if (isset(static::$resolvedInstances[$key])) { + return static::$resolvedInstances[$key]; + } + + $instance = static::$resolver[$key]; + if (!is_object($instance)) { + throw new RuntimeException(sprintf( + 'The facade [%s] instance must be an object, received %s', + get_called_class(), + gettype($instance) + )); + } + + static::$resolvedInstances[$key] = $instance; + return static::$resolvedInstances[$key]; + } + + public static function clearResolvedFacadeInstance(string $key): void + { + unset(static::$resolvedInstances[$key]); + } + + public static function clearResolvedFacadeInstances(): void + { + static::$resolvedInstances = []; + } + + /** + * Handle dynamic, static calls to the object. + * + * @param string $method + * @param mixed[] $args + * @return mixed + */ + public static function __callStatic(string $method, array $args = []) + { + return static::getFacadeInstance()->$method(...$args); + } +} diff --git a/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php b/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php index c40b7df69..84ee46cd7 100644 --- a/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php +++ b/packages/app/src/Charcoal/App/ServiceProvider/AppServiceProvider.php @@ -3,18 +3,19 @@ namespace Charcoal\App\ServiceProvider; // From PSR-7 -use Charcoal\Factory\GenericResolver; use Psr\Http\Message\UriInterface; // From Pimple use Pimple\ServiceProviderInterface; use Pimple\Container; // From Slim use Slim\Http\Uri; -// From 'league/climate' +// From 'league' use League\CLImate\CLImate; // From Mustache use Mustache_LambdaHelper as LambdaHelper; +use Charcoal\Event\ServiceProvider\EventServiceProvider; use Charcoal\Factory\GenericFactory as Factory; +use Charcoal\Factory\GenericResolver; use Charcoal\Cache\ServiceProvider\CacheServiceProvider; use Charcoal\Translator\ServiceProvider\TranslatorServiceProvider; use Charcoal\App\AppConfig; @@ -35,9 +36,9 @@ use Charcoal\App\ServiceProvider\ScriptServiceProvider; use Charcoal\App\ServiceProvider\LoggerServiceProvider; use Charcoal\App\Template\TemplateInterface; -use Charcoal\App\Template\TemplateBuilder; use Charcoal\App\Template\WidgetInterface; use Charcoal\App\Template\WidgetBuilder; +use Charcoal\Object\RevisionServiceProvider; use Charcoal\View\Twig\DebugHelpers as TwigDebugHelpers; use Charcoal\View\Twig\HelpersInterface as TwigHelpersInterface; use Charcoal\View\Twig\UrlHelpers as TwigUrlHelpers; @@ -72,11 +73,13 @@ public function register(Container $container) { $container->register(new CacheServiceProvider()); $container->register(new DatabaseServiceProvider()); + $container->register(new EventServiceProvider()); $container->register(new FilesystemServiceProvider()); $container->register(new LoggerServiceProvider()); $container->register(new ScriptServiceProvider()); $container->register(new TranslatorServiceProvider()); $container->register(new ViewServiceProvider()); + $container->register(new RevisionServiceProvider()); $this->registerKernelServices($container); $this->registerHandlerServices($container); diff --git a/packages/app/tests/Charcoal/App/ServiceProvider/FilesystemServiceProviderTest.php b/packages/app/tests/Charcoal/App/ServiceProvider/FilesystemServiceProviderTest.php index a9a6e9cc4..b745de795 100644 --- a/packages/app/tests/Charcoal/App/ServiceProvider/FilesystemServiceProviderTest.php +++ b/packages/app/tests/Charcoal/App/ServiceProvider/FilesystemServiceProviderTest.php @@ -207,7 +207,9 @@ public function testConfigWithoutTypeThrowsException() private function createAppConfig($defaults = null) { - return new AppConfig(array_replace(['base_path' => sys_get_temp_dir()], $defaults)); + return new AppConfig(array_replace([ + 'base_path' => sys_get_temp_dir() + ], $defaults)); } private function getContainer($defaults = null) diff --git a/packages/cache/src/Charcoal/Cache/CacheConfig.php b/packages/cache/src/Charcoal/Cache/CacheConfig.php index 041df2bc9..ad8724fb6 100644 --- a/packages/cache/src/Charcoal/Cache/CacheConfig.php +++ b/packages/cache/src/Charcoal/Cache/CacheConfig.php @@ -66,7 +66,7 @@ class CacheConfig extends AbstractConfig * * @return array */ - public function defaults() + public function defaults(): array { return [ 'active' => true, diff --git a/packages/cache/src/Charcoal/Cache/Middleware/CacheMiddleware.php b/packages/cache/src/Charcoal/Cache/Middleware/CacheMiddleware.php index 1c94a670c..f71037999 100644 --- a/packages/cache/src/Charcoal/Cache/Middleware/CacheMiddleware.php +++ b/packages/cache/src/Charcoal/Cache/Middleware/CacheMiddleware.php @@ -145,7 +145,7 @@ public function __construct(array $data) * * @return array */ - public function defaults() + public function defaults(): array { return [ 'ttl' => CacheConfig::DAY_IN_SECONDS, diff --git a/packages/cms/tests/Charcoal/Cms/ContainerIntegrationTrait.php b/packages/cms/tests/Charcoal/Cms/ContainerIntegrationTrait.php index 46ee1cedd..1880f36c9 100644 --- a/packages/cms/tests/Charcoal/Cms/ContainerIntegrationTrait.php +++ b/packages/cms/tests/Charcoal/Cms/ContainerIntegrationTrait.php @@ -6,6 +6,7 @@ use Charcoal\App\AppContainer as Container; // From 'charcoal-cms/tests' +use Charcoal\App\Facade\Facade; use Charcoal\Tests\Cms\ContainerProvider; /** @@ -109,5 +110,8 @@ private function setupContainer() $this->container = $container; $this->containerProvider = $provider; + + Facade::clearResolvedFacadeInstances(); + Facade::setFacadeResolver($container); } } diff --git a/packages/cms/tests/Charcoal/Cms/TemplateableTraitTest.php b/packages/cms/tests/Charcoal/Cms/TemplateableTraitTest.php index a53c7aaad..a0b0bd201 100644 --- a/packages/cms/tests/Charcoal/Cms/TemplateableTraitTest.php +++ b/packages/cms/tests/Charcoal/Cms/TemplateableTraitTest.php @@ -1,6 +1,6 @@ $value) { $this->offsetReplace($key, $value); @@ -334,7 +334,7 @@ public function offsetReplace($key, $value) * @param string $path The file to load and add. * @return self */ - public function addFile($path) + public function addFile(string $path): self { $config = $this->loadFile($path); if (is_array($config)) { diff --git a/packages/config/src/Charcoal/Config/ConfigInterface.php b/packages/config/src/Charcoal/Config/ConfigInterface.php index 780dcd384..83ed59e94 100644 --- a/packages/config/src/Charcoal/Config/ConfigInterface.php +++ b/packages/config/src/Charcoal/Config/ConfigInterface.php @@ -18,7 +18,7 @@ interface ConfigInterface extends * * @return array Key-value array of data */ - public function defaults(); + public function defaults(): array; /** * Adds new data, replacing / merging existing data with the same key. @@ -26,7 +26,7 @@ public function defaults(); * @param array|\Traversable $data Key-value array of data to merge. * @return ConfigInterface Chainable */ - public function merge($data); + public function merge(array $data): ConfigInterface; /** * Add a configuration file to the configset. @@ -34,5 +34,5 @@ public function merge($data); * @param string $path The file to load and add. * @return ConfigInterface Chainable */ - public function addFile($path); + public function addFile(string $path): ConfigInterface; } diff --git a/packages/config/tests/Charcoal/Config/Mock/MacroConfig.php b/packages/config/tests/Charcoal/Config/Mock/MacroConfig.php index 9a4dd4933..128abc9a3 100644 --- a/packages/config/tests/Charcoal/Config/Mock/MacroConfig.php +++ b/packages/config/tests/Charcoal/Config/Mock/MacroConfig.php @@ -16,7 +16,7 @@ class MacroConfig extends AbstractConfig /** * @return array */ - public function defaults() + public function defaults(): array { return [ 'foo' => -3, diff --git a/packages/core/src/Charcoal/Model/Events/AbstractModelEvent.php b/packages/core/src/Charcoal/Model/Events/AbstractModelEvent.php new file mode 100644 index 000000000..6e3f19864 --- /dev/null +++ b/packages/core/src/Charcoal/Model/Events/AbstractModelEvent.php @@ -0,0 +1,34 @@ +object = $object; + } + + /** + * @return ModelInterface + */ + public function getObject(): ModelInterface + { + return $this->object; + } +} diff --git a/packages/core/src/Charcoal/Model/Events/WasSaved.php b/packages/core/src/Charcoal/Model/Events/WasSaved.php new file mode 100644 index 000000000..6d0688bb3 --- /dev/null +++ b/packages/core/src/Charcoal/Model/Events/WasSaved.php @@ -0,0 +1,10 @@ + [], @@ -55,7 +55,7 @@ public function defaults($key = null) * @param array|Traversable $data The data to merge. * @return self */ - public function merge($data) + public function merge($data): self { foreach ($data as $key => $val) { if ($key === 'paths') { diff --git a/packages/core/src/Charcoal/Source/DatabaseSourceConfig.php b/packages/core/src/Charcoal/Source/DatabaseSourceConfig.php index 6cfe1042c..1cfbfee80 100644 --- a/packages/core/src/Charcoal/Source/DatabaseSourceConfig.php +++ b/packages/core/src/Charcoal/Source/DatabaseSourceConfig.php @@ -44,7 +44,7 @@ class DatabaseSourceConfig extends SourceConfig /** * @return array */ - public function defaults() + public function defaults(): array { return [ 'type' => 'mysql', diff --git a/packages/core/src/Charcoal/Source/SourceConfig.php b/packages/core/src/Charcoal/Source/SourceConfig.php index 5f4081606..09b9b756a 100644 --- a/packages/core/src/Charcoal/Source/SourceConfig.php +++ b/packages/core/src/Charcoal/Source/SourceConfig.php @@ -19,7 +19,7 @@ class SourceConfig extends AbstractConfig /** * @return array */ - public function defaults() + public function defaults(): array { return [ 'type' => '' diff --git a/packages/core/src/Charcoal/Source/StorableTrait.php b/packages/core/src/Charcoal/Source/StorableTrait.php index 6cd5df751..632b2c484 100644 --- a/packages/core/src/Charcoal/Source/StorableTrait.php +++ b/packages/core/src/Charcoal/Source/StorableTrait.php @@ -2,10 +2,14 @@ namespace Charcoal\Source; -use RuntimeException; -use InvalidArgumentException; -// From 'charcoal-factory' +use Charcoal\App\Facade\Event; use Charcoal\Factory\FactoryInterface; +use Charcoal\Model\Events\WasSaved; +use Charcoal\Model\Events\WasUpdated; +use Charcoal\Model\Events\WillSave; +use Charcoal\Model\Events\WillUpdate; +use InvalidArgumentException; +use RuntimeException; /** * Provides an object with storage interaction. @@ -223,8 +227,9 @@ final public function loadFromQuery($query, array $binds = []) */ final public function save() { + $event = Event::dispatch(new WillSave($this)); $pre = $this->preSave(); - if ($pre === false) { + if ($pre === false || $event->isInterrupted()) { $this->logger->error(sprintf( 'Can not save object "%s:%s"; cancelled by %s::preSave()', $this->objType(), @@ -247,8 +252,9 @@ final public function save() $this->setId($ret); } + $event = Event::dispatch(new WasSaved($this)); $post = $this->postSave(); - if ($post === false) { + if ($post === false || $event->isInterrupted()) { $this->logger->error(sprintf( 'Saved object "%s:%s" but %s::postSave() failed', $this->objType(), @@ -269,8 +275,12 @@ final public function save() */ final public function update(array $keys = null) { + /** @var WillUpdate $event */ + $event = Event::dispatch(new WillUpdate($this)); + + // TODO: remove call to preUpdate $pre = $this->preUpdate($keys); - if ($pre === false) { + if ($pre === false || $event->isInterrupted()) { $this->logger->error(sprintf( 'Can not update object "%s:%s"; cancelled by %s::preUpdate()', $this->objType(), @@ -291,8 +301,12 @@ final public function update(array $keys = null) return false; } + /** @var WasUpdated $event */ + $event = Event::dispatch(new WasUpdated($this)); + + // TODO: remove call to postUpdate $post = $this->postUpdate($keys); - if ($post === false) { + if ($post === false || $event->isInterrupted()) { $this->logger->warning(sprintf( 'Updated object "%s:%s" but %s::postUpdate() failed', $this->objType(), diff --git a/packages/core/tests/Charcoal/CoreContainerIntegrationTrait.php b/packages/core/tests/Charcoal/CoreContainerIntegrationTrait.php index 57ce03768..dd4c5b46d 100644 --- a/packages/core/tests/Charcoal/CoreContainerIntegrationTrait.php +++ b/packages/core/tests/Charcoal/CoreContainerIntegrationTrait.php @@ -3,6 +3,7 @@ namespace Charcoal\Tests; // From Pimple +use Charcoal\App\Facade\Facade; use Pimple\Container; // From 'charcoal-core/tests' @@ -68,5 +69,8 @@ private function setupContainer() $this->container = $container; $this->containerProvider = $provider; + + Facade::clearResolvedFacadeInstances(); + Facade::setFacadeResolver($container); } } diff --git a/packages/core/tests/Charcoal/CoreContainerProvider.php b/packages/core/tests/Charcoal/CoreContainerProvider.php index 6de374c33..be057796d 100644 --- a/packages/core/tests/Charcoal/CoreContainerProvider.php +++ b/packages/core/tests/Charcoal/CoreContainerProvider.php @@ -2,6 +2,7 @@ namespace Charcoal\Tests; +use Charcoal\Event\ServiceProvider\EventServiceProvider; use PDO; // From PSR-3 @@ -52,6 +53,7 @@ public function registerBaseServices(Container $container) $this->registerSource($container); $this->registerLogger($container); $this->registerCache($container); + $container->register(new EventServiceProvider()); } /** diff --git a/packages/core/tests/Charcoal/Mock/BadStorableMock.php b/packages/core/tests/Charcoal/Mock/BadStorableMock.php index 40765d358..5cd154c1e 100644 --- a/packages/core/tests/Charcoal/Mock/BadStorableMock.php +++ b/packages/core/tests/Charcoal/Mock/BadStorableMock.php @@ -9,48 +9,19 @@ /** * */ -class BadStorableMock extends StorableMock +class BadStorableMock extends GenericModel { - const FAIL_AFTER = false; - const FAIL_BEFORE = true; + private bool $failAfter = false; + private bool $failBefore = false; - /** - * Whether to fail before or after an event. - * - * @var boolean - */ - private $fail = self::FAIL_BEFORE; - - /** - * Create new storable mock. - * - * @param boolean $fail TRUE to fail on pre-event, FALSE to fail on post-event. - */ - public function __construct($fail = self::FAIL_BEFORE) + public function failBefore() { - $this->fail = (bool)$fail; - - parent::__construct(); + $this->failBefore = true; } - /** - * Create new storable mock to fail on before events. - * - * @return static - */ - public static function createToFailBefore() - { - return new self(self::FAIL_BEFORE); - } - - /** - * Create new storable mock to fail on after events. - * - * @return static - */ - public static function createToFailAfter() + public function failAfter() { - return new self(self::FAIL_AFTER); + $this->failAfter = true; } /** @@ -61,7 +32,7 @@ public static function createToFailAfter() */ protected function preSave() { - return $this->fail; + return $this->failBefore; } /** @@ -72,7 +43,7 @@ protected function preSave() */ protected function postSave() { - return !$this->fail; + return $this->failAfter; } /** @@ -84,7 +55,7 @@ protected function postSave() */ protected function preUpdate(array $keys = null) { - return $this->fail; + return $this->failBefore; } /** @@ -96,7 +67,7 @@ protected function preUpdate(array $keys = null) */ protected function postUpdate(array $keys = null) { - return !$this->fail; + return $this->failAfter; } /** @@ -107,7 +78,7 @@ protected function postUpdate(array $keys = null) */ protected function preDelete() { - return $this->fail; + return $this->failBefore; } /** @@ -118,6 +89,6 @@ protected function preDelete() */ protected function postDelete() { - return !$this->fail; + return $this->failAfter; } } diff --git a/packages/core/tests/Charcoal/Model/Service/MetadataLoaderTest.php b/packages/core/tests/Charcoal/Model/Service/MetadataLoaderTest.php index fd02a4cae..8ad30073e 100644 --- a/packages/core/tests/Charcoal/Model/Service/MetadataLoaderTest.php +++ b/packages/core/tests/Charcoal/Model/Service/MetadataLoaderTest.php @@ -1,6 +1,6 @@ [ - 'base_path' => dirname(dirname(dirname(dirname(__DIR__)))), + 'base_path' => dirname(__DIR__, 4), ], 'module/classes' => [ 'Charcoal\\Tests\\Mock\\MockModule', diff --git a/packages/core/tests/Charcoal/Source/StorableTraitTest.php b/packages/core/tests/Charcoal/Source/StorableTraitTest.php index 83ae6750d..998054ee4 100644 --- a/packages/core/tests/Charcoal/Source/StorableTraitTest.php +++ b/packages/core/tests/Charcoal/Source/StorableTraitTest.php @@ -2,6 +2,9 @@ namespace Charcoal\Tests\Source; +use Charcoal\Model\Service\MetadataLoader; +use Charcoal\Model\Service\ModelLoaderBuilder; +use Charcoal\Tests\Mock\GenericModel; use InvalidArgumentException; use RuntimeException; @@ -27,12 +30,13 @@ */ class StorableTraitTest extends AbstractTestCase { + use \Charcoal\Tests\CoreContainerIntegrationTrait; use ReflectionsTrait; /** * The tested class. * - * @var StorableMock + * @var GenericModel */ public $obj; @@ -43,7 +47,17 @@ class StorableTraitTest extends AbstractTestCase */ protected function setUp(): void { - $this->obj = new StorableMock(); + $container = $this->getContainer(); + + $this->factory = $container['model/factory']; + $this->obj = $this->factory->get(GenericModel::class); + + $source = $this->obj->source(); + if (!$source->tableExists()) { + $source->createTable(); + } + + // $this->obj = new StorableMock(); } /** @@ -210,7 +224,8 @@ public function testSourceFactory() public function testMissingSourceFactory() { $this->expectException(RuntimeException::class); - $this->callMethod($this->obj, 'sourceFactory'); + $obj = new StorableMock(); + $this->callMethod($obj, 'sourceFactory'); } /** @@ -233,24 +248,19 @@ public function testSource() { $obj = $this->obj; - /** 1. Default state is NULL */ - $this->assertNull($this->getPropertyValue($obj, 'source')); - - /** 2. Create repository if state is NULL */ + /** 1. Create repository if state is NULL */ $src1 = $obj->source(); $this->assertInstanceOf(SourceInterface::class, $src1); - $this->assertSame($src1, $this->getPropertyValue($obj, 'source')); - /** 3. Mutated state */ + /** 2. Mutated state */ $src2 = $this->createSource(); $that = $obj->setSource($src2); $this->assertSame($src2, $obj->source()); - $this->assertSame($src2, $this->getPropertyValue($obj, 'source')); - /** 4. Storable can create a repository */ + /** 3. Storable can create a repository */ $this->assertInstanceOf(SourceInterface::class, $this->callMethod($obj, 'createSource')); - /** 5. Chainable */ + /** 4. Chainable */ $this->assertSame($that, $obj); } @@ -278,13 +288,13 @@ public function testSave() $this->assertTrue($obj->save()); /** 2. Fail Early */ - $obj = BadStorableMock::createToFailBefore(); - $obj->setSource($src); + $obj = $this->factory->create(BadStorableMock::class); + $obj->failBefore(); $this->assertFalse($obj->save()); /** 3. Fail Early */ - $obj = BadStorableMock::createToFailAfter(); - $obj->setSource($src); + $obj = $this->factory->create(BadStorableMock::class); + $obj->failAfter(); $this->assertFalse($obj->save()); } @@ -312,13 +322,13 @@ public function testUpdate() $this->assertTrue($obj->update()); /** 2. Fail Early */ - $obj = BadStorableMock::createToFailBefore(); - $obj->setSource($src); + $obj = $this->factory->create(BadStorableMock::class); + $obj->failBefore(); $this->assertFalse($obj->update()); /** 3. Fail Early */ - $obj = BadStorableMock::createToFailAfter(); - $obj->setSource($src); + $obj = $this->factory->create(BadStorableMock::class); + $obj->failAfter(); $this->assertFalse($obj->update()); } @@ -346,13 +356,15 @@ public function testDelete() $this->assertTrue($obj->delete()); /** 2. Fail Early */ - $obj = BadStorableMock::createToFailBefore(); - $obj->setSource($src); + $obj = $this->factory->create(BadStorableMock::class); + $obj->setId('123'); + $obj->failBefore(); $this->assertFalse($obj->delete()); /** 3. Fail Early */ - $obj = BadStorableMock::createToFailAfter(); - $obj->setSource($src); + $obj = $this->factory->create(BadStorableMock::class); + $obj->setId('123'); + $obj->failAfter(); $this->assertFalse($obj->delete()); } } diff --git a/packages/email/tests/Charcoal/Email/EmailTest.php b/packages/email/tests/Charcoal/Email/EmailTest.php index c03d6f48f..7b78686a6 100644 --- a/packages/email/tests/Charcoal/Email/EmailTest.php +++ b/packages/email/tests/Charcoal/Email/EmailTest.php @@ -1,6 +1,6 @@ $config ]); + +Facade::clearResolvedFacadeInstances(); +Facade::setFacadeResolver($GLOBALS['container']); diff --git a/packages/event/.editorconfig b/packages/event/.editorconfig new file mode 100644 index 000000000..1a2c8bde6 --- /dev/null +++ b/packages/event/.editorconfig @@ -0,0 +1,17 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{md,markdown}] +trim_trailing_whitespace = false + +[*.{ms,mustache}] +insert_final_newline = false diff --git a/packages/event/.gitattributes b/packages/event/.gitattributes new file mode 100644 index 000000000..b8c994f2a --- /dev/null +++ b/packages/event/.gitattributes @@ -0,0 +1,9 @@ +# Ignore for "dist". +/build/travis export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore diff --git a/packages/event/.gitignore b/packages/event/.gitignore new file mode 100644 index 000000000..4a3b017d9 --- /dev/null +++ b/packages/event/.gitignore @@ -0,0 +1,28 @@ +# Package Managers + +composer.phar +composer.lock +/vendor/ +/node_modules/ + +# Logging + +*.log +/logs/ + +# Caching + +/cache/ +/.phplint-cache/ +/.phpunit.result.cache + +# Testing + +phpcs.xml +phpunit.xml + +# Codebase + +/build/docs/ +/build/logs/ +/build/report/ diff --git a/packages/event/LICENSE b/packages/event/LICENSE new file mode 100644 index 000000000..5cd151496 --- /dev/null +++ b/packages/event/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Locomotive Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/event/README.md b/packages/event/README.md new file mode 100644 index 000000000..e6bec3eef --- /dev/null +++ b/packages/event/README.md @@ -0,0 +1,201 @@ +Charcoal Event +============== + +The `Charcoal\Event` module (`charcoal/event`) provides a [`Psr-14`](https://www.php-fig.org/psr/psr-14/) compliant event system using [`League\Event`](https://event.thephpleague.com/3.0/). + + +# How to install + +The preferred (and only supported) way of installing charcoal-event is with **composer**: + +```shell +$ composer require charcoal/event +``` +To install a full Charcoal project, which includes `charcoal-event`: + +```shell +$ composer create-project charcoalphp/boilerplate:@dev --prefer-source +``` + +> Note that charcoal-event is intended to be run along a `charcoal-app` based project. To start from a boilerplate: +> +> ```shell +> $ composer create-project locomotivemtl/charcoal-project-boilerplate + +## Dependencies + +- `PHP 7.4+` + + +## Service Provider + +The following services are provided with the use of [_event_](https://github.com/charcoalphp/event) + +### Services + +* [$container['event/dispatcher']](src/Charcoal/Event/ServiceProvider/EventServiceProvider.php) instance of `\League\Event\Dispatcher` + + +## Configuration + +The configuration of the event module is done via the `event` key of the project configuration. + + +There is two ways to bind listeners to events : + +- The first one is a direct mapping between `Event` classes and listeners : + + +```json +{ + "events": { + "listeners": { + "Namespace\\For\\My\\EventClass": { + "Namespace\\For\\My\\ListenerClass": {...} + } + } + } +} +``` + +- The second one is through a `ListenerSubscriber` which is a class that registers listeners internally. +See the [Subscribers](#subscribers) section for more details. + +```json +{ + "events": { + "subscribers": [ + "Namespace\\For\\My\\ListenerSubscriber" + ] + } +} +``` + +# Usage + +## Events + +An event is a class that can be anything you want. Although, for consistency purposes, certain guidelines can be +applied to ensure ease of use: + +- The [`Charcoal/Event/Event`](src/Charcoal/Event/Event.php) class can be used as a base for a new Event. This base event ensures the event is [Stoppable](https://www.php-fig.org/psr/psr-14/#stoppable-events). +- The class name should be composed of a context and an action applied to it. (e.g) `FileWasUploaded`, `ModelWasUpdated`. +- Since the class implies a context, an event should be able to set said context in its constructor : +```php +class FileWasUploaded extends Event +{ + public function __construct(string $file) + { + $this->file = $file; + + } +} +``` + +## Listeners + +A Listener may be any PHP callable. A Listener MUST have one and only one parameter, which is the Event to which it responds. +See [Psr-14](https://www.php-fig.org/psr/psr-14/#listeners) documentation for more info about listeners. + +In charcoal's context, listeners that are destined to be loaded through json config files should : +- extend [`AbstractEventListener`](src/Charcoal/Event/AbstractEventListener.php) or implement [`EventListenerInterface`](src/Charcoal/Event/EventListenerInterface.php) +- have the `Listener` suffix in its class name + +Config injected listeners are instantiated through a factory and are provided with a `setDependencies()` method for dependency injection. +The `__invoke` method receives the `$event` object as sole parameter. + +If a listener is to be subscribed outside the config, manually, it can be a mere callable function that receives the `event` object. + +To bind a listener to an event, one can manually subscribe the listener using the `event/dispatcher` container key, or use the app config system to attach listeners to events. +By doing so, options can be passed to the listener to dictate its behaviour : + +```json +{ + "events": { + "Namespace\\For\\Some\\Event": { + "Namespace\\For\\Some\\Listener": { + "priority": -1000, + "once": true + } + } + } +} +``` + +### options + +- `priority` : Define the listener priority. Higher priority means the listener will be triggered before lower priority listeners. +[Default: 0] +- `once` : Only trigger the listener once [Default: false] + +## Subscribers + +Listener subscribers are a convenient way to subscribe multiple listeners at once. They allow grouping listener +registrations by concern. Usually, a package will provide a `ListenerSubscriber` to group event listeners and streamline +the registration process. See [League\Event\ListenerSubscriber](https://event.thephpleague.com/3.0/extra-utilities/listener-subscriber/) +for more details about subscribers. + +The [`AbstractListenerSubscriber`](src/Charcoal/Event/AbstractListenerSubscriber.php) class can be extended to create +a listener subscriber. Implement the method `subscribeListeners` and subscribe the listeners on `$acceptor` + +```php +public function subscribeListeners(ListenerRegistry $acceptor): void +{ + $acceptor->subscribeTo(MyEvent::class, $this->createListener(MyListener::class)); +} +``` + +# Development + +To install the development environment: + +```shell +$ composer install --prefer-source +``` + +To run the tests: + +```shell +$ composer test +``` + +## Coding style + +The Charcoal-Admin module follows the Charcoal coding-style: + +- [_PSR-1_](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md), except for + - Method names MUST be declared in `snake_case`. +- [_PSR-2_](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md), except for the PSR-1 requirement.q +- [_PSR-4_](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md), autoloading is therefore provided by _Composer_ +- [_phpDocumentor_](http://phpdoc.org/) + - Add DocBlocks for all classes, methods, and functions; + - For type-hinting, use `boolean` (instead of `bool`), `integer` (instead of `int`), `float` (instead of `double` or `real`); + - Omit the `@return` tag if the method does not return anything. +- Naming conventions + - Read the [phpcs.xml.dist](phpcs.xml.dist) file for all the details. + +> Coding style validation / enforcement can be performed with `composer phpcs`. An auto-fixer is also available with `composer phpcbf`. + + +Every classes, methods and functions should be covered by unit tests. PHP code can be tested with _PHPUnit_ and Javascript code with _QUnit_. + +# Authors + +- Joel Alphonso + +# License + +Charcoal is licensed under the MIT license. See [LICENSE](LICENSE) for details. + + + +## Report Issues + +In case you are experiencing a bug or want to request a new feature head over to the [Charcoal monorepo issue tracker](https://github.com/charcoalphp/charcoal/issues) + + + +## Contribute + +The sources of this package are contained in the Charcoal monorepo. We welcome contributions for this package on [charcoalphp/charcoal](https://github.com/charcoalphp/charcoal). + diff --git a/packages/event/composer.json b/packages/event/composer.json new file mode 100644 index 000000000..73d294200 --- /dev/null +++ b/packages/event/composer.json @@ -0,0 +1,66 @@ +{ + "type": "library", + "name": "charcoal/event", + "description": "Charcoal service provider for events system", + "keywords": ["module", "charcoal", "event"], + "homepage": "https://charcoal.locomotive.ca", + "license": "MIT", + "authors": [ + { + "name": "Locomotive", + "homepage": "https://locomotive.ca" + }, + { + "name": "Joel Alphonso", + "email": "joel@locomotive.ca" + } + ], + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "require": { + "charoal/factory": "^3.2", + "league/event": "^3.0", + "php": "^7.4 || ^8.0", + "pimple/pimple": "^3.0", + "psr/event-dispatcher": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.6", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "autoload": { + "psr-4": { + "Charcoal\\Event\\": "src/Charcoal/Event/" + } + }, + "autoload-dev": { + "psr-4": { + "Charcoal\\Tests\\": "tests/Charcoal/" + } + }, + "scripts": { + "test": [ + "@tests" + ], + "tests": [ + "@phplint", + "@phpcs", + "@phpstan", + "@phpunit" + ], + "phplint": "find src tests -type f -name '*.php' -print0 | xargs -0 -n1 -P8 php -l | grep -v '^No syntax errors detected'; test $? -eq 1", + "phpcs": "php vendor/bin/phpcs -ps --colors src/", + "phpcbf": "php vendor/bin/phpcbf -ps --colors src/", + "phpstan": "php vendor/bin/phpstan analyze -l1 src/", + "phpunit": "php vendor/bin/phpunit --coverage-text" + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/packages/event/phpcs.xml.dist b/packages/event/phpcs.xml.dist new file mode 100644 index 000000000..01c267ac3 --- /dev/null +++ b/packages/event/phpcs.xml.dist @@ -0,0 +1,4 @@ + + + + diff --git a/packages/event/phpunit.xml.dist b/packages/event/phpunit.xml.dist new file mode 100644 index 000000000..5c94aaf08 --- /dev/null +++ b/packages/event/phpunit.xml.dist @@ -0,0 +1,32 @@ + + + + + ./tests/Charcoal + + + + + + ./src/Charcoal + + + + + + + + + + diff --git a/packages/event/src/Charcoal/Event/AbstractEventListener.php b/packages/event/src/Charcoal/Event/AbstractEventListener.php new file mode 100644 index 000000000..c17dc3f2d --- /dev/null +++ b/packages/event/src/Charcoal/Event/AbstractEventListener.php @@ -0,0 +1,25 @@ +setLogger($container['logger']); + } +} diff --git a/packages/event/src/Charcoal/Event/AbstractListenerSubscriber.php b/packages/event/src/Charcoal/Event/AbstractListenerSubscriber.php new file mode 100644 index 000000000..7095a1f42 --- /dev/null +++ b/packages/event/src/Charcoal/Event/AbstractListenerSubscriber.php @@ -0,0 +1,38 @@ +listenerFactory = $container['event/listener/factory']; + } + + /** + * @param $listener + * @return EventListenerInterface + */ + protected function createListener($listener): EventListenerInterface + { + return $this->listenerFactory->create($listener); + } + + abstract public function subscribeListeners(ListenerRegistry $acceptor): void; +} diff --git a/packages/event/src/Charcoal/Event/Event.php b/packages/event/src/Charcoal/Event/Event.php new file mode 100644 index 000000000..0b86986e1 --- /dev/null +++ b/packages/event/src/Charcoal/Event/Event.php @@ -0,0 +1,13 @@ +logger) { + if ($event instanceof HasEventName) { + $this->logger->notice('Event dispatched [' . $event->eventName() . ']', [ + 'event' => get_class($event), + ]); + } else { + $this->logger->notice('Event dispatched', [ + 'event' => get_class($event), + ]); + } + } + + return parent::dispatch($event); + } +} diff --git a/packages/event/src/Charcoal/Event/EventDispatcherBuilder.php b/packages/event/src/Charcoal/Event/EventDispatcherBuilder.php new file mode 100644 index 000000000..b2a02b5b7 --- /dev/null +++ b/packages/event/src/Charcoal/Event/EventDispatcherBuilder.php @@ -0,0 +1,97 @@ +container = $container; + } + + /** + * @param array $listeners + * @param array $subscribers + * @return EventDispatcher + */ + public function build(array $listeners = [], array $subscribers = []): EventDispatcher + { + $dispatcher = new EventDispatcher(); + $dispatcher->setLogger($this->container['logger']); + + $this->registerEventListeners($dispatcher, $listeners); + $this->registerListenerSubscribers($dispatcher, $subscribers); + + return $dispatcher; + } + + /** + * @param EventDispatcherInterface $dispatcher Psr-14 Event Dispatcher Interface + * @param array $listenersByEvent Array of EventListenerInterface attached to event. + * @return void + */ + private function registerEventListeners(EventDispatcherInterface $dispatcher, array $listenersByEvent) + { + foreach ($listenersByEvent as $event => $listeners) { + if (!is_iterable($listeners)) { + throw new InvalidArgumentException(sprintf( + 'Expected iterable map of event listeners for [%s]', + $event + )); + } + + foreach ($listeners as $listener => $options) { + if (!is_string($listener)) { + throw new InvalidArgumentException(sprintf( + 'Expected event listener class string as map key for [%s]', + $event + )); + } + + $listener = $this->container['event/listener/factory']->create($listener); + + $priority = ($options['priority'] ?? 0); + $once = ($options['once'] ?? false); + + if ($once) { + $dispatcher->subscribeOnceTo($event, $listener, $priority); + } else { + $dispatcher->subscribeTo($event, $listener, $priority); + } + } + } + } + + /** + * @param EventDispatcherInterface $dispatcher Psr-14 Event Dispatcher Interface + * @param array> $subscribers Pimple DI container + * @return void + */ + private function registerListenerSubscribers(EventDispatcherInterface $dispatcher, array $subscribers) + { + foreach ($subscribers as $subscriber) { + if (!is_string($subscriber) || !class_exists($subscriber)) { + throw new InvalidArgumentException(sprintf( + 'Expected event subscriber as class string, received %s', + (is_string($subscriber) ? $subscriber : gettype($subscriber)) + )); + } + + $subscriber = $this->container['event/listener-subscriber/factory']->create($subscriber); + + $dispatcher->subscribeListenersFrom($subscriber); + } + } +} diff --git a/packages/event/src/Charcoal/Event/EventDispatcherTrait.php b/packages/event/src/Charcoal/Event/EventDispatcherTrait.php new file mode 100644 index 000000000..833831994 --- /dev/null +++ b/packages/event/src/Charcoal/Event/EventDispatcherTrait.php @@ -0,0 +1,56 @@ +eventDispatcher; + } + + /** + * @param EventDispatcherInterface $eventDispatcher EventDispatcher for EventDispatcherTrait. + * @return self + */ + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): self + { + $this->eventDispatcher = $eventDispatcher; + + return $this; + } + + /** + * Provide all relevant listeners with an event to process. + * + * @param object $event + * The object to process. + * + * @return object + * The Event that was passed, now modified by listeners. + */ + protected function dispatchEvent(object $event): object + { + return $this->getEventDispatcher()->dispatch($event); + } + + /** + * @param array $events + * @return array + */ + protected function dispatchEvents(array $events): array + { + return array_map([$this, 'dispatchEvent'], $events); + } +} diff --git a/packages/event/src/Charcoal/Event/EventListenerInterface.php b/packages/event/src/Charcoal/Event/EventListenerInterface.php new file mode 100644 index 000000000..0d5b06789 --- /dev/null +++ b/packages/event/src/Charcoal/Event/EventListenerInterface.php @@ -0,0 +1,17 @@ +file = $file; + } + + /** + * @return string + */ + public function getFile(): string + { + return $this->file; + } +} diff --git a/packages/event/src/Charcoal/Event/GenericEvent.php b/packages/event/src/Charcoal/Event/GenericEvent.php new file mode 100644 index 000000000..ad2864ab7 --- /dev/null +++ b/packages/event/src/Charcoal/Event/GenericEvent.php @@ -0,0 +1,95 @@ +eventName = $eventName; + $this->subject = $subject; + $this->arguments = $arguments; + } + + /** + * @return string + */ + public function eventName(): string + { + return $this->getEventName(); + } + + /** + * @return mixed + */ + public function getSubject() + { + return $this->subject; + } + + /** + * @param mixed $subject Subject for GenericEvent. + * @return self + */ + public function setSubject($subject): self + { + $this->subject = $subject; + + return $this; + } + + /** + * @return array + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * @param array $arguments Arguments for GenericEvent. + * @return self + */ + public function setArguments(array $arguments): self + { + $this->arguments = $arguments; + + return $this; + } + + /** + * @return string + */ + public function getEventName(): string + { + return $this->eventName; + } + + /** + * @param string $eventName EventName for GenericEvent. + * @return self + */ + public function setEventName(string $eventName): self + { + $this->eventName = $eventName; + + return $this; + } +} diff --git a/packages/event/src/Charcoal/Event/InterruptableEventInterface.php b/packages/event/src/Charcoal/Event/InterruptableEventInterface.php new file mode 100644 index 000000000..3a46c4427 --- /dev/null +++ b/packages/event/src/Charcoal/Event/InterruptableEventInterface.php @@ -0,0 +1,19 @@ +reason = $reason; + + $this->interrupted = true; + } + + public function isInterrupted(): bool + { + return $this->interrupted; + } + + /** + * @return string|Stringable + */ + public function getReasonForInterruption() + { + return $this->reason; + } +} diff --git a/packages/event/src/Charcoal/Event/ServiceProvider/EventServiceProvider.php b/packages/event/src/Charcoal/Event/ServiceProvider/EventServiceProvider.php new file mode 100644 index 000000000..c4b38a699 --- /dev/null +++ b/packages/event/src/Charcoal/Event/ServiceProvider/EventServiceProvider.php @@ -0,0 +1,134 @@ +get('events.listeners') ?? []); + }; + + /** + * Subscribers are classes that implements `\League\Event\ListenerSubscriber` + * It allows to subscribe many grouped listeners at once. + * + * @param Container $container + * @return array + */ + $container['event/subscribers'] = function (Container $container): array { + return ($container['config']->get('events.subscribers') ?? []); + }; + + /** + * @param Container $container The Pimple DI container. + * @return FactoryInterface + */ + $container['event/listener/factory'] = function (Container $container) { + return new GenericFactory([ + 'base_class' => EventListenerInterface::class, + 'resolver_options' => [ + 'suffix' => 'Listener' + ], + 'callback' => function ($listener) use ($container) { + if (is_callable([$listener, 'setDependencies'])) { + $listener->setDependencies($container); + } + } + ]); + }; + + /** + * @param Container $container The Pimple DI container. + * @return FactoryInterface + */ + $container['event/listener-subscriber/factory'] = function (Container $container) { + return new GenericFactory([ + 'base_class' => ListenerSubscriber::class, + 'resolver_options' => [ + 'suffix' => 'Subscriber' + ], + 'callback' => function ($subscriber) use ($container) { + if (is_callable([$subscriber, 'setDependencies'])) { + $subscriber->setDependencies($container); + } + } + ]); + }; + + // The App event services + // ========================================================================== + + /** + * @param Container $container + * @return array + */ + $container['app/event/listeners'] = function (Container $container): array { + if (!$container->offsetExists('admin/config')) { + return []; + } + + return ($container['admin/config']->get('events.listeners') ?? []); + }; + + /** + * Subscribers are classes that implements `\League\Event\ListenerSubscriber` + * It allows to subscribe many grouped listeners at once. + * + * @param Container $container + * @return array + */ + $container['app/event/subscribers'] = function (Container $container): array { + if (!$container->offsetExists('admin/config')) { + return []; + } + + return ($container['admin/config']->get('events.subscribers') ?? []); + }; + + /** + * Build an event dispatcher using admin config. + * + * @param Container $container + * @return EventDispatcher + */ + $container['app/event/dispatcher'] = function (Container $container): EventDispatcher { + /** @var EventDispatcherBuilder $eventDispatcherBuilder */ + $eventDispatcherBuilder = $container['event/dispatcher/builder']; + + return $eventDispatcherBuilder->build( + $container['app/event/listeners'], + $container['app/event/subscribers'] + ); + }; + } +} diff --git a/packages/event/src/Charcoal/Event/StoppableEventTrait.php b/packages/event/src/Charcoal/Event/StoppableEventTrait.php new file mode 100644 index 000000000..6e6717da3 --- /dev/null +++ b/packages/event/src/Charcoal/Event/StoppableEventTrait.php @@ -0,0 +1,38 @@ +propagationStopped; + } + + /** + * Stop the propagation of the event to further listeners. + * The remainder of the subscribed listeners won't be dispatched + * + * @return void + */ + public function stopPropagation() + { + $this->propagationStopped = true; + } +} diff --git a/packages/event/tests/Charcoal/Event/ServiceProvider/EventServiceProviderTest.php b/packages/event/tests/Charcoal/Event/ServiceProvider/EventServiceProviderTest.php new file mode 100644 index 000000000..3207eb676 --- /dev/null +++ b/packages/event/tests/Charcoal/Event/ServiceProvider/EventServiceProviderTest.php @@ -0,0 +1,12 @@ + Default metadata is defined in `metadata/charcoal/object/publishable-interface.json`. -### Revisionable +### Revision Manager -Revisionable objects implement `\Charcoal\Object\Revision\RevisionableInterface`, which can be easily implemented by using `\Charcoal\Object\Revision\RevisionableTrait`. +The Revision Manager is a service that handles every related tasks with keeping revisions of objects implementing `\Charcoal\Model\ModelInterface`. -Revisionable objects create _revisions_ which logs the changes between an object's versions, as _diffs_. +The manager creates _revisions_ which logs the changes between an object's versions, as _diffs_. + +The `\Charcoal\Object\Listener` is a listener available to map a Model to a revision generation. **API** -- `setRevisionEnabled(bool$enabled)` - `revisionEnabled()` - `revisionObject()` - `generateRevision()` - `latestRevision()` -- `revisionNum(integer $revNum)` +- `revisionForNumber(integer $revNum)` - `allRevisions(callable $callback = null)` - `revertToRevision(integer $revNum)` -**Properties (metadata)** +**USAGE** + +```PHP +$revisionMangager->setModel($model)->generateRevision(); +``` + +The revision manager also looks for a configuration in the app config keyed `revisions`. +This config gives projects control over the revision system like disabling the revisions or specifying what models to enable +revisions for. The following example can be used to enable revisions for all content models in a project: + +```JSON +{ + "revisions": { + "enabled": true, + "excludedProperties": [ + "created", + "lastModified", + "createdBy", + "lastModifiedBy", + "active", + "locked", + "requiredAclPermissions", + "position" + ], + "models": { + "Charcoal\\Object\\Content": {} + } + } +} +``` + +**Config options** + +| Key | Description | Type | Default Value | +|--------------------------|---------------------------------------------------------------------------------------------|-----------------------------------------|----------------------------------| +| **enabled** | Enable or not the revisions. | `bool` | `true` | +| **revisionClass** | Change the revision object class. | `string` | `Charcoal\Object\ObjectRevision` | +| **limitPerModel** (TODO) | Define a limit of revisions per model. | `int|null` | `null` | +| **models** | Specify which models to enable revisions for and and what options to apply. See next table. | `object` | `[]` | +| **excludedProperties** | Exclude properties from the revision process. | `string[]` | `[]` | + + +**Models options** + +| Key | Description | Type | Default Value | +|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|----------------------------------| +| **enabled** | Enable or not the revisions for the model. Important to note that class inheritance is respected, meaning enabling revisions for a high order model will also enable revisions for it's children. | `bool` | `true` | +| **revisionClass** | Change the revision object class for a specific model and it's children. | `string` | `Charcoal\Object\ObjectRevision` | +| **limitPerModel** (TODO) | Define a limit of revisions per model. | `int|null` | `null` | +| **properties** | Limits the revision process to only these properties, disregarding property exclusions and inclusions. | `string[]` | `[]` | +| **excludedProperties** | Exclude properties from the revision process. | `string[]` | `[]` | +| **includedProperties** | Include properties in the revision process. By default, all properties are included, so this can be used to include a property that was excluded by a parent. | `string[]` | `[]` | + -_The revisionable behavior does not implement any properties as all logic & data is self-contained in the revisions._ ### Routable @@ -262,16 +313,16 @@ Upon every `update` in _storage_, a revisionable object creates a new *revision* **Revision properties** -| Property | Type | Default | Description | -| ------------------ | ------------ | ---------- | ----------- | -| **target_type** | `string` | `null` | The object type of the target object. -| **target_id** | `string` | `null` | The object idenfiier of the target object. -| **rev_num** | `integer` | `null` | Revision number, (auto-generated). -| **ref_ts** | `date-time` | | -| **rev_user** | `string` | `null` | -| **data_prev** | `structure` | | -| **data_obj** | `structure` | | -| **data_diff** | `structure` | | +| Property | Type | Default | Description | +|-----------------|-------------|---------|--------------------------------------------| +| **target_type** | `string` | `null` | The object type of the target object. | +| **target_id** | `string` | `null` | The object idenfiier of the target object. | +| **rev_num** | `integer` | `null` | Revision number, (auto-generated). | +| **ref_ts** | `date-time` | | | +| **rev_user** | `string` | `null` | | +| **data_prev** | `structure` | | | +| **data_obj** | `structure` | | | +| **data_diff** | `structure` | | | **Revision methods** @@ -286,14 +337,14 @@ It is possible, (typically from the charcoal admin backend), to create *schedule **Schedule properties** -| Property | Type | Default | Description | -| ------------------ | ------------ | ---------- | ----------- | -| **target_type** | `string` | `null` | The object type of the target object. -| **target_id** | `string` | `null` | The object idenfiier of the target object. -| **scheduled_date** | `date-time` | `null` | -| **data_diff** | `structure` | `[]` | -| **processed** | `boolean` | `false` | -| **processed_date** | +| Property | Type | Default | Description | +|--------------------|-------------|---------|--------------------------------------------| +| **target_type** | `string` | `null` | The object type of the target object. | +| **target_id** | `string` | `null` | The object idenfiier of the target object. | +| **scheduled_date** | `date-time` | `null` | | +| **data_diff** | `structure` | `[]` | | +| **processed** | `boolean` | `false` | | +| **processed_date** | | | | **Schedule methods (API)** diff --git a/packages/object/metadata/admin/charcoal/object/object-revision.json b/packages/object/metadata/admin/charcoal/object/object-revision.json index 14fd32a23..f5f2954b2 100644 --- a/packages/object/metadata/admin/charcoal/object/object-revision.json +++ b/packages/object/metadata/admin/charcoal/object/object-revision.json @@ -39,7 +39,7 @@ "form": { "type": "charcoal/admin/widget/object-form", "form_ident": "default", - "target_type": "charcoal/object/object-revision" + "obj_type": "charcoal/object/object-revision" } }, "layout": { @@ -57,7 +57,7 @@ "form": { "type": "charcoal/admin/widget/table", "collection_ident": "default", - "target_type": "charcoal/object/object-revision" + "obj_type": "charcoal/object/object-revision" } }, "layout": { diff --git a/packages/object/src/Charcoal/Object/Content.php b/packages/object/src/Charcoal/Object/Content.php index 32cf80941..2b53f1bf0 100644 --- a/packages/object/src/Charcoal/Object/Content.php +++ b/packages/object/src/Charcoal/Object/Content.php @@ -15,8 +15,6 @@ use Charcoal\Object\ContentInterface; use Charcoal\Object\AuthorableInterface; use Charcoal\Object\AuthorableTrait; -use Charcoal\Object\RevisionableInterface; -use Charcoal\Object\RevisionableTrait; use Charcoal\Object\TimestampableInterface; use Charcoal\Object\TimestampableTrait; @@ -26,11 +24,9 @@ class Content extends AbstractModel implements AuthorableInterface, ContentInterface, - RevisionableInterface, TimestampableInterface { use AuthorableTrait; - use RevisionableTrait; use TranslatorAwareTrait; use TimestampableTrait; @@ -197,11 +193,6 @@ protected function preUpdate(array $properties = null) { parent::preUpdate($properties); - // Content is revisionable - if ($this['revisionEnabled']) { - $this->generateRevision(); - } - // Timestampable propertiees $this->setLastModified('now'); diff --git a/packages/object/src/Charcoal/Object/GenerateRevisionListener.php b/packages/object/src/Charcoal/Object/GenerateRevisionListener.php new file mode 100644 index 000000000..ba2a744fc --- /dev/null +++ b/packages/object/src/Charcoal/Object/GenerateRevisionListener.php @@ -0,0 +1,30 @@ +getObject(); + + $this->revisionManager->setModel($model)->generateRevision(); + } + + public function setDependencies(Container $container) + { + parent::setDependencies($container); + + $this->revisionManager = $container->get('revisions/manager'); + } +} diff --git a/packages/object/src/Charcoal/Object/ObjectRevision.php b/packages/object/src/Charcoal/Object/ObjectRevision.php index 4f847c572..675ec5bba 100644 --- a/packages/object/src/Charcoal/Object/ObjectRevision.php +++ b/packages/object/src/Charcoal/Object/ObjectRevision.php @@ -2,6 +2,7 @@ namespace Charcoal\Object; +use Charcoal\Model\ModelInterface; use InvalidArgumentException; use DateTime; use DateTimeInterface; @@ -14,7 +15,6 @@ use Charcoal\Model\ModelFactoryTrait; // From 'charcoal-object' use Charcoal\Object\ObjectRevisionInterface; -use Charcoal\Object\RevisionableInterface; /** * Represents the changeset of an object. @@ -291,10 +291,11 @@ public function getDataDiff() * 2. Load the current item from DB * 3. Create diff from (1) and (2). * - * @param RevisionableInterface $obj The object to create the revision from. + * @param ModelInterface $obj The object to create the revision from. + * @param array|null $properties List of properties to revision. * @return ObjectRevision Chainable */ - public function createFromObject(RevisionableInterface $obj) + public function createFromObject(ModelInterface $obj, ?array $properties = null) { $prevRev = $this->lastObjectRevision($obj); @@ -307,7 +308,7 @@ public function createFromObject(RevisionableInterface $obj) $this->setRevUser($obj['lastModifiedBy']); } - $this->setDataObj($obj->data()); + $this->setDataObj($obj->data($properties)); $this->setDataPrev($prevRev->getDataObj()); $diff = $this->createDiff(); @@ -382,10 +383,10 @@ public function recursiveDiff(array $array1, array $array2) /** * @todo Should return NULL if source does not exist. * - * @param RevisionableInterface $obj The object to load the last revision of. + * @param ModelInterface $obj The object to load the last revision of. * @return ObjectRevision The last revision for the give object. */ - public function lastObjectRevision(RevisionableInterface $obj) + public function lastObjectRevision(ModelInterface $obj) { $rev = $this->modelFactory()->create(self::class); @@ -410,11 +411,11 @@ public function lastObjectRevision(RevisionableInterface $obj) * * @todo Should return NULL if source does not exist. * - * @param RevisionableInterface $obj Target object. + * @param ModelInterface $obj Target object. * @param integer $revNum The revision number to load. * @return ObjectRevision */ - public function objectRevisionNum(RevisionableInterface $obj, $revNum) + public function objectRevisionNum(ModelInterface $obj, $revNum) { $rev = $this->modelFactory()->create(self::class); diff --git a/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php b/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php index 9206ee66c..349c18108 100644 --- a/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php +++ b/packages/object/src/Charcoal/Object/ObjectRevisionInterface.php @@ -2,8 +2,10 @@ namespace Charcoal\Object; +use Charcoal\Model\ModelInterface; + /** - * Defines a changeset of an object implementing {@see \Charcoal\Object\RevisionableInterface}. + * Defines a changeset of an object implementing {@see \Charcoal\Object\ModelInterface}. * * {@see \Charcoal\Object\ObjectRevision} for a basic implementation. */ @@ -104,10 +106,11 @@ public function getDataDiff(); * 2. Load the current item from DB * 3. Create diff from (1) and (2). * - * @param RevisionableInterface $obj The object to create the revision from. + * @param ModelInterface $obj The object to create the revision from. + * @param array|null $properties List of properties to revision. * @return ObjectRevision Chainable */ - public function createFromObject(RevisionableInterface $obj); + public function createFromObject(ModelInterface $obj, ?array $properties = null); /** * @param array $dataPrev Optional. The previous revision data. @@ -126,17 +129,17 @@ public function createDiff(array $dataPrev, array $dataObj); public function recursiveDiff(array $array1, array $array2); /** - * @param RevisionableInterface $obj The object to load the last revision of. + * @param ModelInterface $obj The object to load the last revision of. * @return ObjectRevision The last revision for the give object. */ - public function lastObjectRevision(RevisionableInterface $obj); + public function lastObjectRevision(ModelInterface $obj); /** * Retrieve a specific object revision, by revision number. * - * @param RevisionableInterface $obj Target object. + * @param ModelInterface $obj Target object. * @param integer $revNum The revision number to load. * @return ObjectRevision */ - public function objectRevisionNum(RevisionableInterface $obj, $revNum); + public function objectRevisionNum(ModelInterface $obj, $revNum); } diff --git a/packages/object/src/Charcoal/Object/RevisionModelConfig.php b/packages/object/src/Charcoal/Object/RevisionModelConfig.php new file mode 100644 index 000000000..25c268301 --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionModelConfig.php @@ -0,0 +1,80 @@ +enabled; + } + + /** + * @return string[] + */ + public function getProperties(): array + { + return $this->properties; + } + + public function hasProperties(): bool + { + return !!count($this->properties); + } + + /** + * @return string[] + */ + public function getExcludedProperties(): array + { + return $this->excludedProperties; + } + + public function hasExcludedProperties(): bool + { + return !!count($this->excludedProperties); + } + + /** + * @return string[] + */ + public function getIncludedProperties(): array + { + return $this->includedProperties; + } + + public function hasIncludedProperties(): bool + { + return (bool)$this->includedProperties; + } + + /** + * @return class-string + */ + public function getRevisionClass(): string + { + return $this->revisionClass; + } +} diff --git a/packages/object/src/Charcoal/Object/RevisionServiceProvider.php b/packages/object/src/Charcoal/Object/RevisionServiceProvider.php new file mode 100644 index 000000000..df50c5e5d --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionServiceProvider.php @@ -0,0 +1,39 @@ +get('revisions'); + + // If the config data is a boolean, it means we only want to affect the enabled state. + if (is_bool($configData)) { + $configData = [ + 'enabled' => $configData, + ]; + } + + return new RevisionsConfig($configData); + }; + + $container['revisions/manager'] = function (Container $container): RevisionsManager { + $services = new ServiceLocator($container, [ + 'revisions/config', + 'model/factory', + 'logger' + ]); + + return new RevisionsManager($services); + }; + } +} diff --git a/packages/object/src/Charcoal/Object/RevisionableInterface.php b/packages/object/src/Charcoal/Object/RevisionableInterface.php deleted file mode 100644 index 6b60497ed..000000000 --- a/packages/object/src/Charcoal/Object/RevisionableInterface.php +++ /dev/null @@ -1,62 +0,0 @@ -revisionEnabled = !!$enabled; - return $this; - } - - /** - * @return boolean - */ - public function getRevisionEnabled() - { - return $this->revisionEnabled; - } - - /** - * Create a revision collection loader. - * - * @return CollectionLoader - */ - public function createRevisionObjectCollectionLoader() - { - $loader = new CollectionLoader([ - 'logger' => $this->logger, - 'factory' => $this->modelFactory(), - 'model' => $this->getRevisionObjectPrototype(), - ]); - - return $loader; - } - - /** - * Create a revision object. - * - * @return ObjectRevisionInterface - */ - public function createRevisionObject() - { - $rev = $this->modelFactory()->create($this->getObjectRevisionClass()); - - return $rev; - } - - /** - * Retrieve the revision object prototype. - * - * @return ObjectRevisionInterface - */ - public function getRevisionObjectPrototype() - { - $proto = $this->modelFactory()->get($this->getObjectRevisionClass()); - - return $proto; - } - - /** - * Set the class name of the object revision model. - * - * @param string $className The class name of the object revision model. - * @throws InvalidArgumentException If the class name is not a string. - * @return AbstractPropertyDisplay Chainable - */ - protected function setObjectRevisionClass($className) - { - if (!is_string($className)) { - throw new InvalidArgumentException( - 'Route class name must be a string.' - ); - } - - $this->objectRevisionClass = $className; - return $this; - } - - /** - * Retrieve the class name of the object revision model. - * - * @return string - */ - public function getObjectRevisionClass() - { - return $this->objectRevisionClass; - } - - /** - * Alias of {@see self::getObjectRevisionClass()}. - * - * @return string - */ - public function objectRevisionClass() - { - return $this->getObjectRevisionClass(); - } - - /** - * @see \Charcoal\Object\ObjectRevision::create_fromObject() - * @return ObjectRevision - */ - public function generateRevision() - { - $rev = $this->createRevisionObject(); - - $rev->createFromObject($this); - if (!empty($rev->getDataDiff())) { - $rev->save(); - } - - return $rev; - } - - /** - * @see \Charcoal\Object\ObejctRevision::lastObjectRevision - * @return ObjectRevision - */ - public function latestRevision() - { - $rev = $this->createRevisionObject(); - $rev = $rev->lastObjectRevision($this); - - return $rev; - } - - /** - * @see \Charcoal\Object\ObejctRevision::objectRevisionNum() - * - * @todo Should return NULL if source does not exist. - * - * @param integer $revNum The revision number. - * @return ObjectRevision - */ - public function revisionNum($revNum) - { - $rev = $this->createRevisionObject(); - $rev = $rev->objectRevisionNum($this, intval($revNum)); - - return $rev; - } - - /** - * Retrieves all revisions for the current objet - * - * @param callable $callback Optional object callback. - * @return array - */ - public function allRevisions(callable $callback = null) - { - $loader = $this->createRevisionObjectCollectionLoader(); - $loader - ->addOrder('revTs', 'desc') - ->addFilters([ - [ - 'property' => 'targetType', - 'value' => $this->objType(), - ], - [ - 'property' => 'targetId', - 'value' => $this->id(), - ], - ]); - - if ($callback !== null) { - $loader->setCallback($callback); - } - - $revisions = $loader->load(); - return $revisions->objects(); - } - - /** - * @param integer $revNum The revision number to revert to. - * @throws InvalidArgumentException If revision number is invalid. - * @return boolean Success / Failure. - */ - public function revertToRevision($revNum) - { - if (!$revNum) { - throw new InvalidArgumentException( - 'Invalid revision number' - ); - } - - $rev = $this->revisionNum(intval($revNum)); - - if (!$rev->id()) { - return false; - } - - if (isset($obj['lastModifiedBy'])) { - $obj['lastModifiedBy'] = $rev->getRevUser(); - } - - $this->setData($rev->getDataObj()); - $this->update(); - - return true; - } - - /** - * Retrieve the object model factory. - * - * @return \Charcoal\Factory\FactoryInterface - */ - abstract public function modelFactory(); - - /** - * @return \Charcoal\Model\MetadataInterface - */ - abstract public function metadata(); -} diff --git a/packages/object/src/Charcoal/Object/RevisionsConfig.php b/packages/object/src/Charcoal/Object/RevisionsConfig.php new file mode 100644 index 000000000..4d849c430 --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionsConfig.php @@ -0,0 +1,158 @@ +prepareRevisionModelConfig($model, $models[$class]); + } + + // If the exact class is not defined in the revisions models key, try to find options from inheritance. + foreach ($this->models as $class => $revisionOptions) { + if ($model instanceof $class) { + return $this->prepareRevisionModelConfig($model, $revisionOptions); + } + } + + return null; + } + + /** + * @param ModelInterface $model + * @param array|boolean $revisionOptions + * @return RevisionModelConfig + */ + private function prepareRevisionModelConfig(ModelInterface $model, $revisionOptions): RevisionModelConfig + { + // If a config is a boolean instead of a data array, it means we only want to affect the enabled state. + if (is_bool($revisionOptions)) { + $revisionOptions = [ + 'enabled' => $revisionOptions, + ]; + } + + $extraOptions = [ + 'excludedProperties' => $this->getExcludedProperties(), + ]; + + // Extract excludedProperties options from the model's ancestors. + foreach ($this->models as $class => $modelConfig) { + if ($model instanceof $class) { + // keep only excludedProperties from ancestors + $modelConfig = array_intersect_key($modelConfig, array_flip(['excludedProperties'])); + $extraOptions = array_merge_recursive($extraOptions, $modelConfig); + } + } + + return new RevisionModelConfig(array_merge($revisionOptions, $extraOptions)); + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return string + */ + public function getRevisionClass(): string + { + return $this->revisionClass; + } + + /** + * @param string $revisionClass RevisionClass for RevisionConfig. + * @return self + */ + public function setRevisionClass(string $revisionClass): self + { + $this->revisionClass = $revisionClass; + + return $this; + } + + /** + * @return int|null + */ + public function getLimitPerModel(): ?int + { + return $this->limitPerModel; + } + + /** + * @param int|null $limitPerModel LimitPerModel for RevisionConfig. + * @return self + */ + public function setLimitPerModel(?int $limitPerModel): self + { + $this->limitPerModel = $limitPerModel; + + return $this; + } + + /** + * @return array + */ + public function getModels(): array + { + return $this->models; + } + + /** + * @param array $models Models for RevisionConfig. + * @return self + */ + public function setModels(array $models): self + { + $this->models = $models; + + return $this; + } + + /** + * @return array + */ + public function getExcludedProperties(): array + { + return $this->excludedProperties; + } + + /** + * @param array $excludedProperties ExcludedProperties for RevisionConfig. + * @return self + */ + public function setExcludedProperties(array $excludedProperties): self + { + $this->excludedProperties = $excludedProperties; + + return $this; + } +} diff --git a/packages/object/src/Charcoal/Object/RevisionsManager.php b/packages/object/src/Charcoal/Object/RevisionsManager.php new file mode 100644 index 000000000..4d6d81938 --- /dev/null +++ b/packages/object/src/Charcoal/Object/RevisionsManager.php @@ -0,0 +1,229 @@ +revisionConfig = $locator->get('revisions/config'); + $this->setModelFactory($locator->get('model/factory')); + $this->setLogger($locator->get('logger')); + } + + public function __invoke(ModelInterface $model): self + { + $this->setModel($model); + + return $this; + } + + public function generateRevision(): ?ObjectRevisionInterface + { + $model = $this->getModel(); + + // Bail early + if ( + !$this->revisionConfig->isEnabled() || + !$this->isRevisionEnabled() + ) { + return null; + } + + $revisionProperties = $this->parseRevisionProperties(); + $revisionObject = $this->createRevisionObject(); + + $revisionObject->createFromObject($model, $revisionProperties); + + if (!empty($revisionObject->getDataDiff())) { + $revisionObject->save(); + } + + return $revisionObject; + } + + public function getLatestRevision(): ObjectRevisionInterface + { + $model = $this->getModel(); + $revision = $this->createRevisionObject(); + + return $revision->lastObjectRevision($model); + } + + /** + * @return ObjectRevisionInterface[] + */ + public function getAllRevisions(callable $callback = null): array + { + $model = $this->getModel(); + $loader = $this->createRevisionObjectCollectionLoader(); + + $loader + ->addOrder('revTs', 'desc') + ->addFilters([ + [ + 'property' => 'targetType', + 'value' => $model->objType(), + ], + [ + 'property' => 'targetId', + 'value' => $model->id(), + ], + ]); + + if ($callback !== null) { + $loader->setCallback($callback); + } + + $revisions = $loader->load(); + + return $revisions->objects(); + } + + public function revertToRevision(int $number): bool + { + $model = $this->getModel(); + $revision = $this->getRevisionFromNumber($number); + + if (!$revision->id()) { + return false; + } + + if (isset($model['lastModifiedBy'])) { + $model['lastModifiedBy'] = $revision->getRevUser(); + } + + $model->setData($revision->getDataObj()); + + return $model->update(); + } + + /** + * @return string[] + */ + public function parseRevisionProperties(): array + { + $model = $this->getModel(); + $modelConfig = $this->getModelRevisionConfig($model); + $properties = array_keys($model->data()); + + if ($modelConfig->hasProperties()) { + return array_intersect($properties, $modelConfig->getProperties()); + } + + if ($modelConfig->hasExcludedProperties()) { + $excludedProperties = $modelConfig->getExcludedProperties(); + + if ($modelConfig->hasIncludedProperties()) { + $includedProperties = $modelConfig->getIncludedProperties(); + $excludedProperties = array_filter($excludedProperties, fn($e) => !in_array($e, $includedProperties)); + } + + return array_filter( + $properties, + fn($n) => !in_array($n, $excludedProperties) + ); + } + + return $properties; + } + + /** + * @return class-string + */ + public function getObjectRevisionClass(): string + { + $modelConfig = $this->getModelRevisionConfig(); + + return $modelConfig->getRevisionClass(); + } + + public function createRevisionObjectCollectionLoader(): CollectionLoader + { + return new CollectionLoader([ + 'logger' => $this->logger, + 'factory' => $this->modelFactory(), + 'model' => $this->getRevisionObjectPrototype($this->getObjectRevisionClass()), + ]); + } + + public function getRevisionObjectPrototype(): ObjectRevisionInterface + { + return $this->modelFactory()->get($this->getObjectRevisionClass()); + } + + public function createRevisionObject(): ObjectRevisionInterface + { + return $this->modelFactory()->create($this->getObjectRevisionClass()); + } + + public function getRevisionFromNumber(int $number): ObjectRevisionInterface + { + return $this->createRevisionObject()->objectRevisionNum($this->getModel(), $number); + } + + public function isRevisionEnabled(): bool + { + $model = $this->getModel(); + $revisionConfig = $this->getModelRevisionConfig($model); + + // If we did not find a config of the value of the config is false, we don't want to revision. + if (!$revisionConfig) { + return false; + } + + return $revisionConfig->isEnabled(); + } + + private function getModelRevisionConfig(): ?RevisionModelConfig + { + $model = $this->getModel(); + + if (!isset($this->modelRevisionConfig[get_class($model)])) { + $this->modelRevisionConfig[get_class($model)] = $this->revisionConfig->buildModelConfig($model); + } + + return $this->modelRevisionConfig[get_class($model)]; + } + + public function getModel(): ModelInterface + { + return $this->model; + } + + public function setModel(ModelInterface $model): self + { + $this->model = $model; + + return $this; + } +} diff --git a/packages/object/tests/Charcoal/Object/ContentTest.php b/packages/object/tests/Charcoal/Object/ContentTest.php index d9f3e68c5..94e8bd0d6 100644 --- a/packages/object/tests/Charcoal/Object/ContentTest.php +++ b/packages/object/tests/Charcoal/Object/ContentTest.php @@ -59,9 +59,6 @@ public function testDefaults() // Authorable properties $this->assertNull($this->obj['createdBy']); $this->assertNull($this->obj['lastModifiedBy']); - - // Revisionable properties - $this->assertTrue($this->obj['revisionEnabled']); } /** diff --git a/packages/object/tests/Charcoal/Object/ObjectRouteTest.php b/packages/object/tests/Charcoal/Object/ObjectRouteTest.php index 9451fe128..d3c168cd6 100644 --- a/packages/object/tests/Charcoal/Object/ObjectRouteTest.php +++ b/packages/object/tests/Charcoal/Object/ObjectRouteTest.php @@ -2,6 +2,8 @@ namespace Charcoal\Tests\Object; +use Charcoal\App\Facade\Facade; +use Charcoal\Event\ServiceProvider\EventServiceProvider; use DateTime; // From Pimple @@ -170,8 +172,11 @@ private function container() $containerProvider->registerBaseServices($container); $containerProvider->registerModelFactory($container); $containerProvider->registerModelCollectionLoader($container); + $container->register(new EventServiceProvider()); $this->container = $container; + Facade::clearResolvedFacadeInstances(); + Facade::setFacadeResolver($container); } return $this->container; diff --git a/packages/property/src/Charcoal/Property/Event/PropertyEvent.php b/packages/property/src/Charcoal/Property/Event/PropertyEvent.php new file mode 100644 index 000000000..89370c3d1 --- /dev/null +++ b/packages/property/src/Charcoal/Property/Event/PropertyEvent.php @@ -0,0 +1,78 @@ +type = $type; + $this->property = $property; + $this->data = $data; + } + + /** + * @return string + */ + public function eventName(): string + { + return $this->generateEventName($this->getType(), $this->getProperty()->type()); + } + + /** + * @param string $event The event name. + * @param string $propertyType The property type. + * @return string + */ + public static function generateEventName(string $event, string $propertyType): string + { + return implode('.', [self::EVENT_PREFIX, $propertyType, $event]); + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * The property triggering the event. + * + * @return PropertyInterface + */ + public function getProperty(): PropertyInterface + { + return $this->property; + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } +} diff --git a/packages/property/tests/Charcoal/Property/AbstractFilePropertyTestCase.php b/packages/property/tests/Charcoal/Property/AbstractFilePropertyTestCase.php index 1fe7f3a25..bf677425a 100644 --- a/packages/property/tests/Charcoal/Property/AbstractFilePropertyTestCase.php +++ b/packages/property/tests/Charcoal/Property/AbstractFilePropertyTestCase.php @@ -3,7 +3,6 @@ namespace Charcoal\Tests\Property; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\FileProperty; use Charcoal\Tests\AbstractTestCase; @@ -59,7 +58,7 @@ public function getFileMapOfFixtures() if ($this->fileMapOfFixtures === null) { $this->fileMapOfFixtures = []; foreach (self::FIXTURES as $filename) { - $this->fileMapOfFixtures[$filename] = $this->getPathToFixture('files/'.$filename); + $this->fileMapOfFixtures[$filename] = $this->getPathToFixture('files/' . $filename); } } @@ -222,7 +221,7 @@ public function testFilesizeFromBadVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/blank.txt'); $this->assertEquals(0, $obj['filesize']); @@ -261,7 +260,7 @@ public function testMimetypeFromBadVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/bad.txt'); $this->assertNull($obj['mimetype']); @@ -277,7 +276,7 @@ public function testMimetypeFromEmptyFile() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/blank.txt'); $this->assertEquals('application/x-empty', $obj['mimetype']); diff --git a/packages/property/tests/Charcoal/Property/AbstractPropertyTest.php b/packages/property/tests/Charcoal/Property/AbstractPropertyTest.php index 8a840cd0d..02b8c2b99 100644 --- a/packages/property/tests/Charcoal/Property/AbstractPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/AbstractPropertyTest.php @@ -6,7 +6,6 @@ use LogicException; use RuntimeException; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\AbstractProperty; use Charcoal\Tests\AbstractTestCase; @@ -254,7 +253,7 @@ public function testMultipleSeparator() $this->assertEquals(',', $this->obj->multipleSeparator()); $this->obj->setMultipleOptions([ - 'separator'=>'/' + 'separator' => '/' ]); $this->assertEquals('/', $this->obj->multipleSeparator()); } diff --git a/packages/property/tests/Charcoal/Property/AudioPropertyTest.php b/packages/property/tests/Charcoal/Property/AudioPropertyTest.php index 9516c518f..f3b3c730f 100644 --- a/packages/property/tests/Charcoal/Property/AudioPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/AudioPropertyTest.php @@ -87,7 +87,7 @@ public function testFilesizeFromVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/buzzer.mp3'); $this->assertEquals(16512, $obj['filesize']); @@ -104,7 +104,7 @@ public function testMimetypeFromVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/buzzer.mp3'); $mime = $obj['mimetype']; diff --git a/packages/property/tests/Charcoal/Property/BooleanPropertyTest.php b/packages/property/tests/Charcoal/Property/BooleanPropertyTest.php index bbe46f710..ae5b430e9 100644 --- a/packages/property/tests/Charcoal/Property/BooleanPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/BooleanPropertyTest.php @@ -3,7 +3,6 @@ namespace Charcoal\Tests\Property; use PDO; - // From 'charcoal-property' use Charcoal\Property\BooleanProperty; use Charcoal\Tests\AbstractTestCase; @@ -63,8 +62,8 @@ public function testDisplayVal() $this->assertEquals('Yes', $this->obj->displayVal(true)); $this->assertEquals('No', $this->obj->displayVal(false)); - $this->assertEquals('V', $this->obj->displayVal(true, ['true_label'=>'V'])); - $this->assertEquals('F', $this->obj->displayVal(false, ['false_label'=>'F'])); + $this->assertEquals('V', $this->obj->displayVal(true, ['true_label' => 'V'])); + $this->assertEquals('F', $this->obj->displayVal(false, ['false_label' => 'F'])); } /** @@ -104,8 +103,8 @@ public function testSetData() { $obj = $this->obj; $data = [ - 'true_label'=>'foo', - 'false_label'=>'bar' + 'true_label' => 'foo', + 'false_label' => 'bar' ]; $ret = $obj->setData($data); diff --git a/packages/property/tests/Charcoal/Property/ColorPropertyTest.php b/packages/property/tests/Charcoal/Property/ColorPropertyTest.php index 1a11f7daf..a739baf2a 100644 --- a/packages/property/tests/Charcoal/Property/ColorPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/ColorPropertyTest.php @@ -6,7 +6,6 @@ use InvalidArgumentException; use PDO; use ReflectionClass; - // From 'charcoal-property' use Charcoal\Property\ColorProperty; use Charcoal\Tests\AbstractTestCase; @@ -73,7 +72,7 @@ public function parseOneFalse() public function parseOneArray() { - $this->assertEquals(['r'=>255, 'g'=>255, 'b'=>255], $this->obj->parseOne([255,255,255])); + $this->assertEquals(['r' => 255, 'g' => 255, 'b' => 255], $this->obj->parseOne([255,255,255])); $this->expectException(InvalidArgumentException::class); $this->obj->parseOne([255]); } @@ -162,8 +161,8 @@ public function colorProviderNoAlpha() ['Red', '#FF0000'], ['RED', '#FF0000'], [[255,0,255], '#FF00FF'], - [['r'=>255, 'g'=>0, 'b'=>255], '#FF00FF'], - [['r'=>255, 'g'=>0, 'b'=>255, 'a'=>0], '#FF00FF'], + [['r' => 255, 'g' => 0, 'b' => 255], '#FF00FF'], + [['r' => 255, 'g' => 0, 'b' => 255, 'a' => 0], '#FF00FF'], ['ABC', '#AABBCC'] ]; } @@ -188,8 +187,8 @@ public function colorProviderAlpha() ['Red', 'rgba(255,0,0,0)'], ['RED', 'rgba(255,0,0,0)'], [[255,0,255], 'rgba(255,0,255,0)'], - [['r'=>255, 'g'=>0, 'b'=>255], 'rgba(255,0,255,0)'], - [['r'=>255, 'g'=>0, 'b'=>255, 'a'=>0], 'rgba(255,0,255,0)'] + [['r' => 255, 'g' => 0, 'b' => 255], 'rgba(255,0,255,0)'], + [['r' => 255, 'g' => 0, 'b' => 255, 'a' => 0], 'rgba(255,0,255,0)'] ]; } diff --git a/packages/property/tests/Charcoal/Property/ContainerIntegrationTrait.php b/packages/property/tests/Charcoal/Property/ContainerIntegrationTrait.php index 5af9bbfba..df3ee878b 100644 --- a/packages/property/tests/Charcoal/Property/ContainerIntegrationTrait.php +++ b/packages/property/tests/Charcoal/Property/ContainerIntegrationTrait.php @@ -3,8 +3,9 @@ namespace Charcoal\Tests\Property; // From Pimple +use Charcoal\App\Facade\Facade; +use Charcoal\Event\ServiceProvider\EventServiceProvider; use Pimple\Container; - // From 'charcoal-property/tests' use Charcoal\Tests\Property\ContainerProvider; @@ -65,8 +66,12 @@ private function setupContainer() $provider->registerPropertyFactory($container); $provider->registerModelFactory($container); $provider->registerModelCollectionLoader($container); + $container->register(new EventServiceProvider()); $this->container = $container; $this->containerProvider = $provider; + + Facade::clearResolvedFacadeInstances(); + Facade::setFacadeResolver($container); } } diff --git a/packages/property/tests/Charcoal/Property/ContainerProvider.php b/packages/property/tests/Charcoal/Property/ContainerProvider.php index 59512fc74..295f18d70 100644 --- a/packages/property/tests/Charcoal/Property/ContainerProvider.php +++ b/packages/property/tests/Charcoal/Property/ContainerProvider.php @@ -3,36 +3,27 @@ namespace Charcoal\Tests\Property; use PDO; - // From PSR-3 use Psr\Log\NullLogger; - // From 'cache/void-adapter' (PSR-6) use Cache\Adapter\Void\VoidCachePool; - // From 'tedivm/stash' (PSR-6) use Stash\Pool; use Stash\Driver\Ephemeral; - // From Pimple use Pimple\Container; - // From 'symfony/translator' use Symfony\Component\Translation\Loader\ArrayLoader; - // From 'charcoal-factory' use Charcoal\Factory\GenericFactory as Factory; - // From 'charcoal-core' use Charcoal\Model\Service\MetadataLoader; use Charcoal\Loader\CollectionLoader; use Charcoal\Source\DatabaseSource; - // From 'charcoal-view' use Charcoal\View\GenericView; use Charcoal\View\Mustache\MustacheEngine; use Charcoal\View\Mustache\MustacheLoader; - // From 'charcoal-translator' use Charcoal\Translator\LocalesManager; use Charcoal\Translator\Translator; @@ -65,8 +56,8 @@ public function registerBaseServices(Container $container) public function registerConfig(Container $container) { $container['config'] = [ - 'base_path' => realpath(__DIR__.'/../../..'), - 'public_path' => realpath(__DIR__.'/../../..'), + 'base_path' => realpath(__DIR__ . '/../../..'), + 'public_path' => realpath(__DIR__ . '/../../..'), ]; } diff --git a/packages/property/tests/Charcoal/Property/DateTimePropertyTest.php b/packages/property/tests/Charcoal/Property/DateTimePropertyTest.php index eb0b1c001..100da858e 100644 --- a/packages/property/tests/Charcoal/Property/DateTimePropertyTest.php +++ b/packages/property/tests/Charcoal/Property/DateTimePropertyTest.php @@ -6,7 +6,6 @@ use DateTime; use Exception; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\DateTimeProperty; use Charcoal\Tests\AbstractTestCase; @@ -144,7 +143,7 @@ public function testDisplayVal() $this->assertEquals('2015/09/01', $this->obj->displayVal(new DateTime('September 1st, 2015'))); // Test with custom format passed as parameter - $this->assertEquals('2017/12/12', $this->obj->displayVal('December 12, 2017', ['format'=>'Y/m/d'])); + $this->assertEquals('2017/12/12', $this->obj->displayVal('December 12, 2017', ['format' => 'Y/m/d'])); // Test with null value $this->assertEquals('', $this->obj->displayVal(null)); diff --git a/packages/property/tests/Charcoal/Property/FilePropertyTest.php b/packages/property/tests/Charcoal/Property/FilePropertyTest.php index 2978d68ca..f79594d3d 100644 --- a/packages/property/tests/Charcoal/Property/FilePropertyTest.php +++ b/packages/property/tests/Charcoal/Property/FilePropertyTest.php @@ -5,10 +5,8 @@ use PDO; use InvalidArgumentException; use ReflectionClass; - // From 'charcoal-core' use Charcoal\Validator\ValidatorInterface as Validator; - // From 'charcoal-property' use Charcoal\Property\FileProperty; @@ -91,7 +89,7 @@ public function testFilesizeFromVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/document.txt'); $this->assertEquals(743, $obj['filesize']); @@ -106,7 +104,7 @@ public function testMimetypeFromVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/document.txt'); $this->assertEquals('text/plain', $obj['mimetype']); @@ -183,7 +181,7 @@ public function testValidateMimetypes( ) { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['acceptedMimetypes'] = $acceptedMimetypes; $obj['l10n'] = $l10n; $obj['multiple'] = $multiple; @@ -220,7 +218,7 @@ public function testValidateFilesizes( ) { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['maxFilesize'] = $maxFilesize; $obj['l10n'] = $l10n; $obj['multiple'] = $multiple; @@ -423,7 +421,7 @@ public function provideDataForValidateMimetypes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['panda.png'].'] has unacceptable MIME type [image/png]', + 'File [' . $paths['panda.png'] . '] has unacceptable MIME type [image/png]', ], ], ], @@ -435,7 +433,7 @@ public function provideDataForValidateMimetypes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['nonexistent.txt'].'] not found or MIME type unrecognizable', + 'File [' . $paths['nonexistent.txt'] . '] not found or MIME type unrecognizable', ], ], ], @@ -458,7 +456,7 @@ public function provideDataForValidateMimetypes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['panda.png'].'] has unacceptable MIME type [image/png]', + 'File [' . $paths['panda.png'] . '] has unacceptable MIME type [image/png]', ], ], ], @@ -484,13 +482,13 @@ public function provideDataForValidateMimetypes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['panda.png'].'] has unacceptable MIME type [image/png]', + 'File [' . $paths['panda.png'] . '] has unacceptable MIME type [image/png]', ], ], ], 'text/plain, l10n + multiple #1' => [ 'propertyValues' => [ - 'en' => $paths['document.txt'].','.$paths['todo.txt'], + 'en' => $paths['document.txt'] . ',' . $paths['todo.txt'], 'fr' => [ $paths['stuff.txt'], $paths['draft.txt'] ], ], 'propertyL10n' => false, @@ -501,7 +499,7 @@ public function provideDataForValidateMimetypes() ], 'text/plain, l10n + multiple #2' => [ 'propertyValues' => [ - 'en' => $paths['document.txt'].','.$paths['scream.wav'], + 'en' => $paths['document.txt'] . ',' . $paths['scream.wav'], 'fr' => [ $paths['stuff.txt'], $paths['cat.jpg'] ], ], 'propertyL10n' => false, @@ -510,8 +508,8 @@ public function provideDataForValidateMimetypes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['scream.wav'].'] has unacceptable MIME type [audio/%s]', - 'File ['.$paths['cat.jpg'].'] has unacceptable MIME type [image/%s]', + 'File [' . $paths['scream.wav'] . '] has unacceptable MIME type [audio/%s]', + 'File [' . $paths['cat.jpg'] . '] has unacceptable MIME type [image/%s]', ], ], ], @@ -569,7 +567,7 @@ public function provideDataForValidateFilesizes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['panda.png'].'] exceeds maximum file size [%s]', + 'File [' . $paths['panda.png'] . '] exceeds maximum file size [%s]', ], ], ], @@ -581,7 +579,7 @@ public function provideDataForValidateFilesizes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['nonexistent.txt'].'] not found or size unknown', + 'File [' . $paths['nonexistent.txt'] . '] not found or size unknown', ], ], ], @@ -604,7 +602,7 @@ public function provideDataForValidateFilesizes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['panda.png'].'] exceeds maximum file size [%s]', + 'File [' . $paths['panda.png'] . '] exceeds maximum file size [%s]', ], ], ], @@ -630,13 +628,13 @@ public function provideDataForValidateFilesizes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['panda.png'].'] exceeds maximum file size [%s]', + 'File [' . $paths['panda.png'] . '] exceeds maximum file size [%s]', ], ], ], 'max 10kB, l10n + multiple #1' => [ 'propertyValues' => [ - 'en' => $paths['document.txt'].','.$paths['todo.txt'], + 'en' => $paths['document.txt'] . ',' . $paths['todo.txt'], 'fr' => [ $paths['stuff.txt'], $paths['draft.txt'] ], ], 'propertyL10n' => false, @@ -647,7 +645,7 @@ public function provideDataForValidateFilesizes() ], 'max 10kB, l10n + multiple #2' => [ 'propertyValues' => [ - 'en' => $paths['document.txt'].','.$paths['scream.wav'], + 'en' => $paths['document.txt'] . ',' . $paths['scream.wav'], 'fr' => [ $paths['stuff.txt'], $paths['panda.png'] ], ], 'propertyL10n' => false, @@ -656,8 +654,8 @@ public function provideDataForValidateFilesizes() 'assertValidationReturn' => false, 'assertValidationResults' => [ Validator::ERROR => [ - 'File ['.$paths['scream.wav'].'] exceeds maximum file size [%s]', - 'File ['.$paths['panda.png'].'] exceeds maximum file size [%s]', + 'File [' . $paths['scream.wav'] . '] exceeds maximum file size [%s]', + 'File [' . $paths['panda.png'] . '] exceeds maximum file size [%s]', ], ], ], diff --git a/packages/property/tests/Charcoal/Property/FixturesTrait.php b/packages/property/tests/Charcoal/Property/FixturesTrait.php index e04cdbb25..a457e4aa5 100644 --- a/packages/property/tests/Charcoal/Property/FixturesTrait.php +++ b/packages/property/tests/Charcoal/Property/FixturesTrait.php @@ -15,7 +15,7 @@ trait FixturesTrait */ public function getPathToFixture($file) { - return $this->getPathToFixtures().'/'.ltrim($file, '/'); + return $this->getPathToFixtures() . '/' . ltrim($file, '/'); } /** diff --git a/packages/property/tests/Charcoal/Property/IdPropertyTest.php b/packages/property/tests/Charcoal/Property/IdPropertyTest.php index ff05b7748..bd66cc4af 100644 --- a/packages/property/tests/Charcoal/Property/IdPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/IdPropertyTest.php @@ -5,7 +5,6 @@ use PDO; use DomainException; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\IdProperty; use Charcoal\Tests\AbstractTestCase; @@ -52,7 +51,7 @@ public function testSetData() { $ret = $this->obj->setData( [ - 'mode'=>'uniqid' + 'mode' => 'uniqid' ] ); $this->assertSame($ret, $this->obj); diff --git a/packages/property/tests/Charcoal/Property/ImagePropertyTest.php b/packages/property/tests/Charcoal/Property/ImagePropertyTest.php index 845c5208d..be4c0e5b5 100644 --- a/packages/property/tests/Charcoal/Property/ImagePropertyTest.php +++ b/packages/property/tests/Charcoal/Property/ImagePropertyTest.php @@ -3,7 +3,6 @@ namespace Charcoal\Tests\Property; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\ImageProperty; @@ -92,7 +91,7 @@ public function testFilesizeFromVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/panda.png'); $this->assertEquals(170276, $obj['filesize']); @@ -107,7 +106,7 @@ public function testMimetypeFromVal() { $obj = $this->obj; - $obj['uploadPath'] = $this->getPathToFixtures().'/files'; + $obj['uploadPath'] = $this->getPathToFixtures() . '/files'; $obj['val'] = $this->getPathToFixture('files/panda.png'); $this->assertEquals('image/png', $obj['mimetype']); @@ -119,13 +118,13 @@ public function testMimetypeFromVal() public function testSetEffects() { $this->assertEquals([], $this->obj['effects']); - $ret = $this->obj->setEffects([['type'=>'blur', 'sigma'=>'1']]); + $ret = $this->obj->setEffects([['type' => 'blur', 'sigma' => '1']]); $this->assertSame($ret, $this->obj); - $this->obj['effects'] = [['type'=>'blur', 'sigma'=>'1'], ['type'=>'revert']]; + $this->obj['effects'] = [['type' => 'blur', 'sigma' => '1'], ['type' => 'revert']]; $this->assertEquals(2, count($this->obj['effects'])); - $this->obj->set('effects', [['type'=>'blur', 'sigma'=>'1']]); + $this->obj->set('effects', [['type' => 'blur', 'sigma' => '1']]); $this->assertEquals(1, count($this->obj['effects'])); $this->assertEquals(1, count($this->obj['effects'])); @@ -138,11 +137,11 @@ public function testAddEffect() { $this->assertEquals(0, count($this->obj['effects'])); - $ret = $this->obj->addEffect(['type'=>'grayscale']); + $ret = $this->obj->addEffect(['type' => 'grayscale']); $this->assertSame($ret, $this->obj); $this->assertEquals(1, count($this->obj['effects'])); - $this->obj->addEffect(['type'=>'blur', 'sigma'=>1]); + $this->obj->addEffect(['type' => 'blur', 'sigma' => 1]); $this->assertEquals(2, count($this->obj['effects'])); } diff --git a/packages/property/tests/Charcoal/Property/IpPropertyTest.php b/packages/property/tests/Charcoal/Property/IpPropertyTest.php index 63056fe5f..2692c9300 100644 --- a/packages/property/tests/Charcoal/Property/IpPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/IpPropertyTest.php @@ -3,7 +3,6 @@ namespace Charcoal\Tests\Property; use PDO; - // From 'charcoal-property' use Charcoal\Property\IpProperty; use Charcoal\Tests\AbstractTestCase; diff --git a/packages/property/tests/Charcoal/Property/LangPropertyTest.php b/packages/property/tests/Charcoal/Property/LangPropertyTest.php index bdf9ac286..b0186261b 100644 --- a/packages/property/tests/Charcoal/Property/LangPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/LangPropertyTest.php @@ -4,7 +4,6 @@ use PDO; use ReflectionClass; - // From 'charcoal-property' use Charcoal\Property\LangProperty; use Charcoal\Tests\AbstractTestCase; diff --git a/packages/property/tests/Charcoal/Property/Mocks/GenericModel.php b/packages/property/tests/Charcoal/Property/Mocks/GenericModel.php index ae900ac45..3b471734f 100644 --- a/packages/property/tests/Charcoal/Property/Mocks/GenericModel.php +++ b/packages/property/tests/Charcoal/Property/Mocks/GenericModel.php @@ -11,10 +11,8 @@ // From Pimple use Pimple\Container; - // From 'charcoal-core' use Charcoal\Model\AbstractModel; - // From 'charcoal-translator' use Charcoal\Translator\Translation; use Charcoal\Translator\TranslatorAwareTrait; diff --git a/packages/property/tests/Charcoal/Property/ObjectPropertyTest.php b/packages/property/tests/Charcoal/Property/ObjectPropertyTest.php index 43e67c399..e7d759c8f 100644 --- a/packages/property/tests/Charcoal/Property/ObjectPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/ObjectPropertyTest.php @@ -6,18 +6,14 @@ use ReflectionClass; use RuntimeException; use InvalidArgumentException; - // From PSR-6 use Psr\Cache\CacheItemPoolInterface; - // From 'charcoal-core' use Charcoal\Loader\CollectionLoader; use Charcoal\Model\Service\ModelLoader; use Charcoal\Source\StorableInterface; - // From 'charcoal-factory' use Charcoal\Factory\FactoryInterface; - // From 'charcoal-property' use Charcoal\Property\ObjectProperty; use Charcoal\Tests\AbstractTestCase; @@ -314,7 +310,7 @@ public function testDisplayVal() $this->obj->setL10n(false); $this->obj->setMultiple(true); - $expected = 'Foo, '.self::OBJ_2.', Baz, Qux, Xyz'; + $expected = 'Foo, ' . self::OBJ_2 . ', Baz, Qux, Xyz'; $actual = $this->obj->displayVal(implode(',', array_keys($objs))); $this->assertEquals($expected, $actual); diff --git a/packages/property/tests/Charcoal/Property/PropertyFieldTest.php b/packages/property/tests/Charcoal/Property/PropertyFieldTest.php index c97f77728..2ca01f612 100644 --- a/packages/property/tests/Charcoal/Property/PropertyFieldTest.php +++ b/packages/property/tests/Charcoal/Property/PropertyFieldTest.php @@ -4,7 +4,6 @@ use PDO; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\PropertyField; use Charcoal\Tests\AbstractTestCase; diff --git a/packages/property/tests/Charcoal/Property/SelectablePropertyTraitTest.php b/packages/property/tests/Charcoal/Property/SelectablePropertyTraitTest.php index 162435816..ff0b9cff3 100644 --- a/packages/property/tests/Charcoal/Property/SelectablePropertyTraitTest.php +++ b/packages/property/tests/Charcoal/Property/SelectablePropertyTraitTest.php @@ -3,10 +3,8 @@ namespace Charcoal\Tests\Property; use ReflectionClass; - // From 'charcoal-translator' use Charcoal\Translator\Translation; - // From 'charcoal-property' use Charcoal\Property\SelectablePropertyTrait; use Charcoal\Tests\AbstractTestCase; diff --git a/packages/property/tests/Charcoal/Property/SpritePropertyTest.php b/packages/property/tests/Charcoal/Property/SpritePropertyTest.php index 4de3258f2..e406112ce 100644 --- a/packages/property/tests/Charcoal/Property/SpritePropertyTest.php +++ b/packages/property/tests/Charcoal/Property/SpritePropertyTest.php @@ -3,7 +3,6 @@ namespace Charcoal\Tests\Property; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\SpriteProperty; use Charcoal\Tests\AbstractTestCase; diff --git a/packages/property/tests/Charcoal/Property/StorablePropertyTraitTest.php b/packages/property/tests/Charcoal/Property/StorablePropertyTraitTest.php index 4dcf5a96f..94acfde44 100644 --- a/packages/property/tests/Charcoal/Property/StorablePropertyTraitTest.php +++ b/packages/property/tests/Charcoal/Property/StorablePropertyTraitTest.php @@ -3,7 +3,6 @@ namespace Charcoal\Tests\Property; use ReflectionMethod; - // From 'charcoal-property' use Charcoal\Property\GenericProperty; use Charcoal\Property\PropertyField; diff --git a/packages/property/tests/Charcoal/Property/StringPropertyTest.php b/packages/property/tests/Charcoal/Property/StringPropertyTest.php index 18bcc58ce..c0053477d 100644 --- a/packages/property/tests/Charcoal/Property/StringPropertyTest.php +++ b/packages/property/tests/Charcoal/Property/StringPropertyTest.php @@ -3,10 +3,8 @@ namespace Charcoal\Tests\Property; use PDO; - // From 'charcoal-translator' use Charcoal\Translator\Translation; - // From 'charcoal-property' use Charcoal\Property\StringProperty; use Charcoal\Tests\AbstractTestCase; diff --git a/packages/property/tests/Charcoal/Property/StructurePropertyTest.php b/packages/property/tests/Charcoal/Property/StructurePropertyTest.php index 0ca1ae2f8..5e31f3536 100644 --- a/packages/property/tests/Charcoal/Property/StructurePropertyTest.php +++ b/packages/property/tests/Charcoal/Property/StructurePropertyTest.php @@ -4,7 +4,6 @@ use Exception; use InvalidArgumentException; - // From 'charcoal-property' use Charcoal\Property\StructureProperty; use Charcoal\Tests\AbstractTestCase; @@ -69,7 +68,7 @@ public function testParseOneString() $this->assertEquals('', $this->obj->parseOne('')); // $this->assertEquals('foo', $this->obj->parseOne('foo')); $this->assertEquals(['foo'], $this->obj->parseOne('["foo"]')); - $this->assertEquals(['foo'=>'bar'], $this->obj->parseOne('{"foo":"bar"}')); + $this->assertEquals(['foo' => 'bar'], $this->obj->parseOne('{"foo":"bar"}')); } public function testSqlType() diff --git a/packages/translator/src/Charcoal/Translator/LocalesConfig.php b/packages/translator/src/Charcoal/Translator/LocalesConfig.php index 7640c6111..f042e59d4 100644 --- a/packages/translator/src/Charcoal/Translator/LocalesConfig.php +++ b/packages/translator/src/Charcoal/Translator/LocalesConfig.php @@ -37,7 +37,7 @@ class LocalesConfig extends AbstractConfig /** * @return array */ - public function defaults() + public function defaults(): array { return [ 'languages' => [ diff --git a/packages/translator/src/Charcoal/Translator/Middleware/LanguageMiddleware.php b/packages/translator/src/Charcoal/Translator/Middleware/LanguageMiddleware.php index 388157429..4ebaec46b 100644 --- a/packages/translator/src/Charcoal/Translator/Middleware/LanguageMiddleware.php +++ b/packages/translator/src/Charcoal/Translator/Middleware/LanguageMiddleware.php @@ -119,7 +119,7 @@ public function __construct(array $data) * * @return array */ - public function defaults() + public function defaults(): array { return [ 'default_language' => null, diff --git a/packages/translator/src/Charcoal/Translator/Script/TranslationParserScript.php b/packages/translator/src/Charcoal/Translator/Script/TranslationParserScript.php index 094e4237f..a6de9e8f0 100644 --- a/packages/translator/src/Charcoal/Translator/Script/TranslationParserScript.php +++ b/packages/translator/src/Charcoal/Translator/Script/TranslationParserScript.php @@ -266,7 +266,7 @@ public function output() return $this->output; } $output = $this->argOrInput('output'); - $this->output = (string)$output; + $this->output = rtrim((string)$output, '/') . DIRECTORY_SEPARATOR; return $this->output; } @@ -357,7 +357,6 @@ public function getTranslations() */ public function getTranslationsFromPath($path, $fileType) { - // remove vendor/locomotivemtl/charcoal-app $base = $this->appConfig->get('base_path'); $glob = $this->globRecursive($base . DIRECTORY_SEPARATOR . $path . '*.' . $fileType); $regex = $this->regEx($fileType); @@ -445,7 +444,7 @@ public function paths() { if (!$this->paths) { $this->paths = $this->appConfig->get('translator.parser.view.paths') ?: - $this->appConfig->get('view.paths'); + $this->appConfig->resolveValues($this->appConfig->get('view.paths')); /** @todo Hardcoded; Change this! */ $this->paths[] = 'src/'; @@ -495,7 +494,7 @@ public function toCSV(array $translations) if (!file_exists($filePath)) { mkdir($filePath, 0755, true); } - $file = fopen($base . $output . $domain . '.' . $lang . '.csv', 'w'); + $file = fopen($base . DIRECTORY_SEPARATOR . $output . $domain . '.' . $lang . '.csv', 'w'); if (!$file) { continue; } diff --git a/packages/translator/src/Charcoal/Translator/TranslatorConfig.php b/packages/translator/src/Charcoal/Translator/TranslatorConfig.php index 9063debdb..29a2d1e77 100644 --- a/packages/translator/src/Charcoal/Translator/TranslatorConfig.php +++ b/packages/translator/src/Charcoal/Translator/TranslatorConfig.php @@ -52,7 +52,7 @@ class TranslatorConfig extends AbstractConfig /** * @return array */ - public function defaults() + public function defaults(): array { return [ 'loaders' => [ diff --git a/packages/translator/tests/Charcoal/Translator/Middleware/LanguageMiddlewareTest.php b/packages/translator/tests/Charcoal/Translator/Middleware/LanguageMiddlewareTest.php index 3e09b5309..05e3d4830 100644 --- a/packages/translator/tests/Charcoal/Translator/Middleware/LanguageMiddlewareTest.php +++ b/packages/translator/tests/Charcoal/Translator/Middleware/LanguageMiddlewareTest.php @@ -1,6 +1,6 @@ obj; - $cb = function($o) { + $cb = function() { return 'foo'; }; $ret = $obj->setInputCallback($cb); diff --git a/packages/ui/tests/Charcoal/Ui/FormGroup/GenericFormGroupTest.php b/packages/ui/tests/Charcoal/Ui/FormGroup/GenericFormGroupTest.php index 9d8791a0b..550a659d3 100644 --- a/packages/ui/tests/Charcoal/Ui/FormGroup/GenericFormGroupTest.php +++ b/packages/ui/tests/Charcoal/Ui/FormGroup/GenericFormGroupTest.php @@ -1,6 +1,6 @@