diff --git a/Makefile b/Makefile index 84a42c12..5fd77d4e 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,9 @@ test-cache: phpunit test-clock: PHPUNIT_TESTSUITE=clock test-clock: phpunit +test-cookie: PHPUNIT_TESTSUITE=cookie +test-cookie: phpunit + test-cqrs: PHPUNIT_TESTSUITE=cqrs test-cqrs: phpunit diff --git a/bard.json b/bard.json index 8837d7fa..a3d28056 100644 --- a/bard.json +++ b/bard.json @@ -13,6 +13,10 @@ "path": "src/SonsOfPHP/Component/Clock", "repository": "git@github.com:SonsOfPHP/clock.git" }, + { + "path": "src/SonsOfPHP/Component/Cookie", + "repository": "git@github.com:SonsOfPHP/cookie.git" + }, { "path": "src/SonsOfPHP/Component/Cqrs", "repository": "git@github.com:SonsOfPHP/cqrs.git" @@ -101,6 +105,10 @@ "path": "src/SonsOfPHP/Contract/Common", "repository": "git@github.com:SonsOfPHP/common-contract.git" }, + { + "path": "src/SonsOfPHP/Contract/Cookie", + "repository": "git@github.com:SonsOfPHP/cookie-contract.git" + }, { "path": "src/SonsOfPHP/Contract/Cqrs", "repository": "git@github.com:SonsOfPHP/cqrs-contract.git" diff --git a/composer.json b/composer.json index d42f8f7f..40bf6b78 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,8 @@ "psr/log-implementation": "^1.0 || ^2.0 || ^3.0", "sonsofphp/logger-implementation": "0.3.x-dev", "sonsofphp/pager-implementation": "0.3.x-dev", - "psr/link-implementation": "^1.0 || ^2.0" + "psr/link-implementation": "^1.0 || ^2.0", + "sonsofphp/cookie-implementation": "0.3.x-dev" }, "require": { "php": ">=8.1", @@ -74,7 +75,8 @@ "twig/twig": "^3.0", "ext-intl": "*", "doctrine/collections": "^2", - "doctrine/orm": "^2" + "doctrine/orm": "^2", + "sonsofphp/cookie-contract": "0.3.x-dev" }, "replace": { "sonsofphp/bard": "self.version", @@ -110,13 +112,16 @@ "sonsofphp/money-twig": "self.version", "sonsofphp/pager-doctrine-collections": "self.version", "sonsofphp/pager-doctrine-dbal": "self.version", - "sonsofphp/pager-doctrine-orm": "self.version" + "sonsofphp/pager-doctrine-orm": "self.version", + "sonsofphp/cookie": "self.version", + "sonsofphp/cookie-contract": "self.version" }, "autoload": { "psr-4": { "SonsOfPHP\\Bard\\": "src/SonsOfPHP/Bard/src", "SonsOfPHP\\Component\\Cache\\": "src/SonsOfPHP/Component/Cache", "SonsOfPHP\\Component\\Clock\\": "src/SonsOfPHP/Component/Clock", + "SonsOfPHP\\Component\\Cookie\\": "src/SonsOfPHP/Component/Cookie", "SonsOfPHP\\Component\\Cqrs\\": "src/SonsOfPHP/Component/Cqrs", "SonsOfPHP\\Bundle\\Cqrs\\": "src/SonsOfPHP/Bundle/Cqrs", "SonsOfPHP\\Bridge\\Symfony\\Cqrs\\": "src/SonsOfPHP/Bridge/Symfony/Cqrs", @@ -139,6 +144,7 @@ "SonsOfPHP\\Bridge\\Doctrine\\ORM\\Pager\\": "src/SonsOfPHP/Bridge/Doctrine/ORM/Pager", "SonsOfPHP\\Component\\Version\\": "src/SonsOfPHP/Component/Version", "SonsOfPHP\\Contract\\Common\\": "src/SonsOfPHP/Contract/Common", + "SonsOfPHP\\Contract\\Cookie\\": "src/SonsOfPHP/Contract/Cookie", "SonsOfPHP\\Contract\\Cqrs\\": "src/SonsOfPHP/Contract/Cqrs", "SonsOfPHP\\Contract\\EventSourcing\\": "src/SonsOfPHP/Contract/EventSourcing", "SonsOfPHP\\Contract\\FeatureToggle\\": "src/SonsOfPHP/Contract/FeatureToggle", @@ -152,6 +158,7 @@ "src/SonsOfPHP/Bard/Tests", "src/SonsOfPHP/Component/Cache/Tests", "src/SonsOfPHP/Component/Clock/Tests", + "src/SonsOfPHP/Component/Cookie/Tests", "src/SonsOfPHP/Component/Cqrs/Tests", "src/SonsOfPHP/Bundle/Cqrs/Tests", "src/SonsOfPHP/Bridge/Symfony/Cqrs/Tests", diff --git a/mkdocs.yml b/mkdocs.yml index b8b29ee1..9c0fc364 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -79,6 +79,7 @@ nav: - components/index.md - Cache: components/cache/index.md - Clock: components/clock/index.md + - Cookie: components/cookie/index.md - CQRS: - components/cqrs/index.md - Event Dispatcher: components/event-dispatcher/index.md @@ -118,5 +119,6 @@ nav: - Contracts: - contracts/index.md - Common: contracts/common/index.md + - Cookie: contracts/cookie/index.md - Cqrs: contracts/cqrs/index.md - Pager: contracts/pager/index.md diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5ab54b33..db3a9659 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -30,6 +30,10 @@ src/SonsOfPHP/Component/Clock/Tests + + src/SonsOfPHP/Component/Cookie/Tests + + src/SonsOfPHP/Bridge/*/Cqrs/Tests diff --git a/src/SonsOfPHP/Component/Cookie/.gitattributes b/src/SonsOfPHP/Component/Cookie/.gitattributes new file mode 100644 index 00000000..84c7add0 --- /dev/null +++ b/src/SonsOfPHP/Component/Cookie/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/SonsOfPHP/Component/Cookie/.gitignore b/src/SonsOfPHP/Component/Cookie/.gitignore new file mode 100644 index 00000000..5414c2c6 --- /dev/null +++ b/src/SonsOfPHP/Component/Cookie/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor/ diff --git a/src/SonsOfPHP/Component/Cookie/Cookie.php b/src/SonsOfPHP/Component/Cookie/Cookie.php new file mode 100644 index 00000000..5b97ca82 --- /dev/null +++ b/src/SonsOfPHP/Component/Cookie/Cookie.php @@ -0,0 +1,163 @@ + + */ +class Cookie implements CookieInterface +{ + public function __construct( + private string $name, + private string $value = '', + private array $options = ['expires' => 0, 'secure' => false, 'httponly' => false], + ) {} + + /** + * {@inheritdoc} + */ + public function withName(string $name): static + { + if ($name === $this->name) { + return $this; + } + + $that = clone $this; + $that->name = $name; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withValue(string $value): static + { + if ($value === $this->value) { + return $this; + } + + $that = clone $this; + $that->value = $value; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withPath(string $path): static + { + if (array_key_exists('path', $this->options) && $path === $this->options['path']) { + return $this; + } + + $that = clone $this; + $that->options['path'] = $path; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withDomain(string $domain): static + { + if (array_key_exists('domain', $this->options) && $domain === $this->options['domain']) { + return $this; + } + + $that = clone $this; + $that->options['domain'] = $domain; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withSecure(bool $secure): static + { + if (array_key_exists('secure', $this->options) && $secure === $this->options['secure']) { + return $this; + } + + $that = clone $this; + $that->options['secure'] = $secure; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withHttpOnly(bool $httpOnly): static + { + if (array_key_exists('httponly', $this->options) && $httpOnly === $this->options['httponly']) { + return $this; + } + + $that = clone $this; + $that->options['httponly'] = $httpOnly; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withSameSite(string $sameSite): static + { + if (array_key_exists('samesite', $this->options) && $sameSite === $this->options['samesite']) { + return $this; + } + + if (!in_array(strtolower($sameSite), ['none', 'lax', 'strict'])) { + throw new \InvalidArgumentException('Invalid value for $sameSite'); + } + + $that = clone $this; + $that->options['samesite'] = $sameSite; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function withExpires(\DateTimeImmutable|int|string $expires): static + { + if (is_string($expires)) { + $expires = (new \DateTimeImmutable($expires))->format('U'); + } elseif ($expires instanceof \DateTimeImmutable) { + $expires = $expires->format('U'); + } + + if (array_key_exists('expires', $this->options) && $expires === $this->options['expires']) { + return $this; + } + + $that = clone $this; + $that->options['expires'] = $expires; + + return $that; + } + + /** + * {@inheritdoc} + */ + public function send(bool $raw = false): void + { + if (true === $raw) { + // raw dog those values + setrawcookie($this->name, $this->value, $this->options); + return; + } + + setcookie($this->name, $this->value, $this->options); + } +} diff --git a/src/SonsOfPHP/Component/Cookie/LICENSE b/src/SonsOfPHP/Component/Cookie/LICENSE new file mode 100644 index 00000000..39238382 --- /dev/null +++ b/src/SonsOfPHP/Component/Cookie/LICENSE @@ -0,0 +1,19 @@ +Copyright 2022 to Present Joshua Estes + +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/src/SonsOfPHP/Component/Cookie/README.md b/src/SonsOfPHP/Component/Cookie/README.md new file mode 100644 index 00000000..2245d139 --- /dev/null +++ b/src/SonsOfPHP/Component/Cookie/README.md @@ -0,0 +1,16 @@ +Sons of PHP - Cookie +==================== + +## Learn More + +* [Documentation][docs] +* [Contributing][contributing] +* [Report Issues][issues] and [Submit Pull Requests][pull-requests] in the [Mother Repository][mother-repo] +* Get Help & Support using [Discussions][discussions] + +[discussions]: https://github.com/orgs/SonsOfPHP/discussions +[mother-repo]: https://github.com/SonsOfPHP/sonsofphp +[contributing]: https://docs.sonsofphp.com/contributing/ +[docs]: https://docs.sonsofphp.com/components/cookie/ +[issues]: https://github.com/SonsOfPHP/sonsofphp/issues?q=is%3Aopen+is%3Aissue+label%3ACookie +[pull-requests]: https://github.com/SonsOfPHP/sonsofphp/pulls?q=is%3Aopen+is%3Apr+label%3ACookie diff --git a/src/SonsOfPHP/Component/Cookie/Tests/CookieTest.php b/src/SonsOfPHP/Component/Cookie/Tests/CookieTest.php new file mode 100644 index 00000000..2caf52dd --- /dev/null +++ b/src/SonsOfPHP/Component/Cookie/Tests/CookieTest.php @@ -0,0 +1,104 @@ +assertInstanceOf(CookieInterface::class, $cookie); + } + + /** + * @covers ::withName + */ + public function testWithName(): void + { + $cookie = new Cookie('test'); + + $this->assertSame($cookie, $cookie->withName('test')); + $this->assertNotSame($cookie, $cookie->withName('test2')); + } + + /** + * @covers ::withValue + */ + public function testWithValue(): void + { + $cookie = new Cookie('test', 'value'); + + $this->assertSame($cookie, $cookie->withValue('value')); + $this->assertNotSame($cookie, $cookie->withValue('value2')); + } + + /** + * @covers ::withPath + */ + public function testWithPath(): void + { + $cookie = (new Cookie('test'))->withPath('/'); + + $this->assertSame($cookie, $cookie->withPath('/')); + $this->assertNotSame($cookie, $cookie->withPath('/testing')); + } + + /** + * @covers ::withDomain + */ + public function testWithDomain(): void + { + $cookie = (new Cookie('test'))->withDomain('sonsofphp.com'); + + $this->assertSame($cookie, $cookie->withDomain('sonsofphp.com')); + $this->assertNotSame($cookie, $cookie->withDomain('docs.sonsofphp.com')); + } + + /** + * @covers ::withSecure + */ + public function testWithSecure(): void + { + $cookie = new Cookie('test'); + + $this->assertSame($cookie, $cookie->withSecure(false)); + $this->assertNotSame($cookie, $cookie->withSecure(true)); + } + + /** + * @covers ::withHttpOnly + */ + public function testWithHttpOnly(): void + { + $cookie = new Cookie('test'); + + $this->assertSame($cookie, $cookie->withHttpOnly(false)); + $this->assertNotSame($cookie, $cookie->withHttpOnly(true)); + } + + /** + * @covers ::withSameSite + */ + public function testWithSameSite(): void + { + $cookie = (new Cookie('test'))->withSameSite('none'); + + $this->assertSame($cookie, $cookie->withSameSite('none')); + $this->assertNotSame($cookie, $cookie->withSameSite('strict')); + } +} diff --git a/src/SonsOfPHP/Component/Cookie/composer.json b/src/SonsOfPHP/Component/Cookie/composer.json new file mode 100644 index 00000000..96bc1774 --- /dev/null +++ b/src/SonsOfPHP/Component/Cookie/composer.json @@ -0,0 +1,54 @@ +{ + "name": "sonsofphp/cookie", + "type": "library", + "description": "Manage Cookies with ease", + "keywords": [ + "cookie" + ], + "homepage": "https://github.com/SonsOfPHP/cookie", + "license": "MIT", + "authors": [ + { + "name": "Joshua Estes", + "email": "joshua@sonsofphp.com" + } + ], + "support": { + "issues": "https://github.com/SonsOfPHP/sonsofphp/issues", + "forum": "https://github.com/orgs/SonsOfPHP/discussions", + "docs": "https://docs.sonsofphp.com" + }, + "autoload": { + "psr-4": { + "SonsOfPHP\\Component\\Cookie\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": ">=8.1", + "sonsofphp/cookie-contract": "0.3.x-dev" + }, + "provide": { + "sonsofphp/cookie-implementation": "0.3.x-dev" + }, + "extra": { + "sort-packages": true, + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/JoshuaEstes" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/packagist-sonsofphp-sonsofphp" + } + ] +} \ No newline at end of file diff --git a/src/SonsOfPHP/Component/Pager/README.md b/src/SonsOfPHP/Component/Pager/README.md index 79b97e64..94b314db 100644 --- a/src/SonsOfPHP/Component/Pager/README.md +++ b/src/SonsOfPHP/Component/Pager/README.md @@ -1,5 +1,5 @@ -Sons of PHP - Logger -==================== +Sons of PHP - Pager +=================== ## Learn More @@ -11,6 +11,6 @@ Sons of PHP - Logger [discussions]: https://github.com/orgs/SonsOfPHP/discussions [mother-repo]: https://github.com/SonsOfPHP/sonsofphp [contributing]: https://docs.sonsofphp.com/contributing/ -[docs]: https://docs.sonsofphp.com/components/logger/ -[issues]: https://github.com/SonsOfPHP/sonsofphp/issues?q=is%3Aopen+is%3Aissue+label%3ALogger -[pull-requests]: https://github.com/SonsOfPHP/sonsofphp/pulls?q=is%3Aopen+is%3Apr+label%3ALogger +[docs]: https://docs.sonsofphp.com/components/pager/ +[issues]: https://github.com/SonsOfPHP/sonsofphp/issues?q=is%3Aopen+is%3Aissue+label%3APager +[pull-requests]: https://github.com/SonsOfPHP/sonsofphp/pulls?q=is%3Aopen+is%3Apr+label%3APager diff --git a/src/SonsOfPHP/Contract/Cookie/.gitattributes b/src/SonsOfPHP/Contract/Cookie/.gitattributes new file mode 100644 index 00000000..3a01b372 --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/.gitattributes @@ -0,0 +1,2 @@ +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/SonsOfPHP/Contract/Cookie/.gitignore b/src/SonsOfPHP/Contract/Cookie/.gitignore new file mode 100644 index 00000000..d8a7996a --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor/ diff --git a/src/SonsOfPHP/Contract/Cookie/CookieExceptionInterface.php b/src/SonsOfPHP/Contract/Cookie/CookieExceptionInterface.php new file mode 100644 index 00000000..90c06107 --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/CookieExceptionInterface.php @@ -0,0 +1,10 @@ + + */ +interface CookieExceptionInterface {} diff --git a/src/SonsOfPHP/Contract/Cookie/CookieInterface.php b/src/SonsOfPHP/Contract/Cookie/CookieInterface.php new file mode 100644 index 00000000..36c59c0a --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/CookieInterface.php @@ -0,0 +1,55 @@ + + */ +interface CookieInterface +{ + /** + * Set's the cookie name + * + * If the $name is the same, it will return the same object, however if the + * $name is different than the current $name, it will return a new instance + * of cookie + */ + public function withName(string $name): static; + + /** + */ + public function withValue(string $value): static; + + /** + */ + public function withPath(string $path): static; + + /** + */ + public function withDomain(string $domain): static; + + /** + */ + public function withSecure(bool $secure): static; + + /** + */ + public function withHttpOnly(bool $httpOnly): static; + + /** + * @throws CookieExceptionInterface if argument is invalid + */ + public function withSameSite(string $sameSite): static; + + /** + * @throws CookieExceptionInterface + */ + public function withExpires(\DateTimeImmutable|int|string $expires): static; + + /** + * @throws CookieExceptionInterface is something went wrong + */ + public function send(bool $raw = false): void; +} diff --git a/src/SonsOfPHP/Contract/Cookie/CookieManagerInterface.php b/src/SonsOfPHP/Contract/Cookie/CookieManagerInterface.php new file mode 100644 index 00000000..66b8c859 --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/CookieManagerInterface.php @@ -0,0 +1,17 @@ + + */ +interface CookieManagerInterface +{ + public function get(string $name): CookieInterface; + + public function has(string $name): bool; + + public function remove(string $name): bool; +} diff --git a/src/SonsOfPHP/Contract/Cookie/LICENSE b/src/SonsOfPHP/Contract/Cookie/LICENSE new file mode 100644 index 00000000..39238382 --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/LICENSE @@ -0,0 +1,19 @@ +Copyright 2022 to Present Joshua Estes + +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/src/SonsOfPHP/Contract/Cookie/README.md b/src/SonsOfPHP/Contract/Cookie/README.md new file mode 100644 index 00000000..4f25aaa6 --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/README.md @@ -0,0 +1,16 @@ +Sons of PHP - Cookie Contract +============================= + +## Learn More + +* [Documentation][docs] +* [Contributing][contributing] +* [Report Issues][issues] and [Submit Pull Requests][pull-requests] in the [Mother Repository][mother-repo] +* Get Help & Support using [Discussions][discussions] + +[discussions]: https://github.com/orgs/SonsOfPHP/discussions +[mother-repo]: https://github.com/SonsOfPHP/sonsofphp +[contributing]: https://docs.sonsofphp.com/contributing/ +[docs]: https://docs.sonsofphp.com/contracts/cookie/ +[issues]: https://github.com/SonsOfPHP/sonsofphp/issues?q=is%3Aopen+is%3Aissue+label%3ACookie +[pull-requests]: https://github.com/SonsOfPHP/sonsofphp/pulls?q=is%3Aopen+is%3Apr+label%3ACookie diff --git a/src/SonsOfPHP/Contract/Cookie/composer.json b/src/SonsOfPHP/Contract/Cookie/composer.json new file mode 100644 index 00000000..6aed6a6c --- /dev/null +++ b/src/SonsOfPHP/Contract/Cookie/composer.json @@ -0,0 +1,52 @@ +{ + "name": "sonsofphp/cookie-contract", + "type": "library", + "description": "", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "homepage": "https://github.com/SonsOfPHP/cookie-contract", + "license": "MIT", + "authors": [ + { + "name": "Joshua Estes", + "email": "joshua@sonsofphp.com" + } + ], + "support": { + "issues": "https://github.com/SonsOfPHP/sonsofphp/issues", + "forum": "https://github.com/orgs/SonsOfPHP/discussions", + "docs": "https://docs.sonsofphp.com" + }, + "autoload": { + "psr-4": { + "SonsOfPHP\\Contract\\Cookie\\": "" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": ">=8.1" + }, + "extra": { + "sort-packages": true, + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/JoshuaEstes" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/packagist-sonsofphp-sonsofphp" + } + ] +} \ No newline at end of file