From 1b29b0b9c762d5700eb2bda97812cd5e0831a416 Mon Sep 17 00:00:00 2001 From: Rodolfo Berrios Date: Sat, 7 Sep 2019 19:23:35 -0400 Subject: [PATCH] Track --- .gitattributes | 2 + .gitignore | 13 + .htaccess | 23 + .php-cs | 108 +++ Chevereto-Chevere/LICENSE | 9 + Chevereto-Chevere/bootstrap.php | 74 ++ Chevereto-Chevere/composer.json | 30 + Chevereto-Chevere/phpunit.php | 11 + Chevereto-Chevere/phpunit.xml | 8 + .../resources/functions/dump.php | 29 + Chevereto-Chevere/src/Api/Api.php | 85 ++ Chevereto-Chevere/src/Api/Endpoint.php | 88 +++ Chevereto-Chevere/src/Api/Maker.php | 231 ++++++ .../src/Api/src/FilterIterator.php | 58 ++ Chevereto-Chevere/src/App/App.php | 164 ++++ Chevereto-Chevere/src/App/Checkout.php | 35 + Chevereto-Chevere/src/App/Loader.php | 304 ++++++++ Chevereto-Chevere/src/App/Parameters.php | 123 +++ Chevereto-Chevere/src/ArrayFile/ArrayFile.php | 145 ++++ .../src/ArrayFile/ArrayFileCallback.php | 48 ++ Chevereto-Chevere/src/Cache/Cache.php | 126 +++ Chevereto-Chevere/src/CallableWrap.php | 287 +++++++ Chevereto-Chevere/src/Console/Cli.php | 124 +++ Chevereto-Chevere/src/Console/Command.php | 92 +++ .../src/Console/Commands/BuildCommand.php | 44 ++ .../src/Console/Commands/InspectCommand.php | 152 ++++ .../src/Console/Commands/RequestCommand.php | 187 +++++ .../src/Console/Commands/RunCommand.php | 193 +++++ Chevereto-Chevere/src/Console/Console.php | 135 ++++ .../src/Console/SymfonyCommand.php | 50 ++ .../src/Contracts/Api/ApiContract.php | 21 + .../src/Contracts/Api/MakerContract.php | 30 + .../Contracts/Api/src/EndpointContract.php | 27 + .../Api/src/FilterIteratorContract.php | 35 + .../src/Contracts/App/AppContract.php | 50 ++ .../src/Contracts/App/CheckoutContract.php | 19 + .../src/Contracts/App/LoaderContract.php | 59 ++ .../src/Contracts/App/ParametersContract.php | 21 + .../src/Contracts/Console/CliContract.php | 46 ++ .../src/Contracts/Console/CommandContract.php | 27 + .../src/Contracts/Console/ConsoleContract.php | 50 ++ .../Console/SymfonyCommandContract.php | 312 ++++++++ .../Controller/ArgumentsWrapContract.php | 21 + .../Controller/ControllerContract.php | 30 + .../Contracts/Controller/InspectContract.php | 24 + .../src/Contracts/DataContract.php | 45 ++ .../src/Contracts/Http/MethodContract.php | 23 + .../src/Contracts/Http/MethodsContract.php | 28 + .../src/Contracts/Http/ResponseContract.php | 41 + .../Http/Symfony/RequestContract.php | 731 ++++++++++++++++++ .../Http/Symfony/ResponseContract.php | 566 ++++++++++++++ .../src/Contracts/Render/RenderContract.php | 19 + .../Contracts/Route/PathValidateContract.php | 23 + .../src/Contracts/Route/RouteContract.php | 96 +++ .../src/Contracts/Route/WildcardsContract.php | 27 + .../src/Contracts/Router/ResolverContract.php | 23 + .../src/Contracts/Router/RouterContract.php | 31 + .../Contracts/Runtime/RuntimeSetContract.php | 25 + .../src/Contracts/ToArrayContract.php | 22 + .../src/Controller/ArgumentsWrap.php | 92 +++ .../src/Controller/Controller.php | 105 +++ Chevereto-Chevere/src/Controller/Inspect.php | 272 +++++++ .../src/Controller/Relationship.php | 31 + Chevereto-Chevere/src/Controller/Resource.php | 31 + .../src/Controllers/Api/GetController.php | 90 +++ .../src/Controllers/Api/HeadController.php | 72 ++ .../src/Controllers/Api/OptionsController.php | 78 ++ .../src/Controllers/HeadController.php | 41 + Chevereto-Chevere/src/Data/Data.php | 106 +++ .../src/Data/Traits/DataAccessTrait.php | 28 + .../src/Data/Traits/DataKeyTrait.php | 28 + .../src/ExceptionHandler/ErrorHandler.php | 27 + .../src/ExceptionHandler/ExceptionHandler.php | 223 ++++++ .../src/ExceptionHandler/src/Formatter.php | 318 ++++++++ .../src/ExceptionHandler/src/Output.php | 191 +++++ .../src/ExceptionHandler/src/Stack.php | 73 ++ .../src/ExceptionHandler/src/Style.php | 28 + .../src/ExceptionHandler/src/Template.php | 61 ++ .../ExceptionHandler/src/TemplatedStrings.php | 135 ++++ .../src/ExceptionHandler/src/TraceEntry.php | 187 +++++ .../src/ExceptionHandler/src/Wrap.php | 75 ++ Chevereto-Chevere/src/File.php | 59 ++ .../src/FileReturn/FileReturn.php | 235 ++++++ Chevereto-Chevere/src/FromString.php | 23 + Chevereto-Chevere/src/Handler.php | 74 ++ Chevereto-Chevere/src/Hooking/Hook.php | 305 ++++++++ Chevereto-Chevere/src/Hooking/Hookable.php | 129 ++++ Chevereto-Chevere/src/Http/Http.php | 71 ++ Chevereto-Chevere/src/Http/Method.php | 75 ++ Chevereto-Chevere/src/Http/Methods.php | 55 ++ Chevereto-Chevere/src/Http/Request.php | 20 + .../src/Http/Request/RequestException.php | 39 + Chevereto-Chevere/src/Http/Response.php | 105 +++ .../ControllerRelationshipInterface.php | 22 + .../ControllerResourceInterface.php | 22 + .../src/Interfaces/CreateFromString.php | 22 + .../src/Interfaces/HandlerInterface.php | 21 + .../src/Interfaces/MiddlewareInterface.php | 19 + .../src/Interfaces/PrintableInterface.php | 23 + .../src/Interfaces/RenderableInterface.php | 19 + Chevereto-Chevere/src/Json.php | 100 +++ Chevereto-Chevere/src/JsonApi/Data.php | 59 ++ Chevereto-Chevere/src/JsonApi/JsonApi.php | 118 +++ .../src/JsonApi/Objects/Error.php | 0 .../src/JsonApi/Objects/JsonApi.php | 0 .../src/JsonApi/Objects/Links.php | 0 .../src/JsonApi/Objects/Meta.php | 0 .../src/JsonApi/Objects/Resource.php | 0 Chevereto-Chevere/src/Log.php | 236 ++++++ Chevereto-Chevere/src/Message.php | 96 +++ Chevereto-Chevere/src/Path/Path.php | 194 +++++ Chevereto-Chevere/src/Path/PathHandle.php | 177 +++++ Chevereto-Chevere/src/Route/PathValidate.php | 93 +++ Chevereto-Chevere/src/Route/Route.php | 281 +++++++ Chevereto-Chevere/src/Route/Set.php | 159 ++++ Chevereto-Chevere/src/Route/Wildcard.php | 96 +++ .../Exception/RouteNotFoundException.php | 19 + Chevereto-Chevere/src/Router/Maker.php | 194 +++++ Chevereto-Chevere/src/Router/Resolver.php | 45 ++ Chevereto-Chevere/src/Router/Router.php | 103 +++ Chevereto-Chevere/src/Runtime/Runtime.php | 41 + .../src/Runtime/Sets/RuntimeSetDebug.php | 38 + .../Runtime/Sets/RuntimeSetDefaultCharset.php | 36 + .../Runtime/Sets/RuntimeSetErrorHandler.php | 46 ++ .../Sets/RuntimeSetExceptionHandler.php | 46 ++ .../src/Runtime/Sets/RuntimeSetLocale.php | 36 + .../src/Runtime/Sets/RuntimeSetPrecision.php | 36 + .../src/Runtime/Sets/RuntimeSetTimeZone.php | 48 ++ .../src/Runtime/Sets/RuntimeSetUriScheme.php | 37 + .../src/Runtime/Traits/RuntimeSet.php | 46 ++ Chevereto-Chevere/src/Stopwatch.php | 102 +++ .../src/Traits/CallableTrait.php | 81 ++ .../src/Traits/HookableTrait.php | 129 ++++ .../src/Traits/PrintableTrait.php | 56 ++ Chevereto-Chevere/src/Type.php | 144 ++++ Chevereto-Chevere/src/Utility/Arr.php | 127 +++ Chevereto-Chevere/src/Utility/Benchmark.php | 300 +++++++ Chevereto-Chevere/src/Utility/Bytes.php | 102 +++ Chevereto-Chevere/src/Utility/Color.php | 90 +++ Chevereto-Chevere/src/Utility/DateTime.php | 143 ++++ Chevereto-Chevere/src/Utility/Number.php | 64 ++ Chevereto-Chevere/src/Utility/Random.php | 66 ++ Chevereto-Chevere/src/Utility/Str.php | 385 +++++++++ Chevereto-Chevere/src/Validate.php | 57 ++ .../src/VarDump/ConsoleVarDump.php | 30 + Chevereto-Chevere/src/VarDump/Dumper.php | 228 ++++++ Chevereto-Chevere/src/VarDump/HtmlVarDump.php | 45 ++ .../src/VarDump/PlainVarDump.php | 25 + Chevereto-Chevere/src/VarDump/VarDump.php | 53 ++ .../src/VarDump/VarDumpAbstract.php | 271 +++++++ Chevereto-Chevere/src/VarDump/src/Pallete.php | 53 ++ .../src/VarDump/src/Template.php | 23 + Chevereto-Chevere/src/VarDump/src/Wrapper.php | 95 +++ Chevereto-Chevere/tests/Api/MakerTest.php | 50 ++ Chevereto-Chevere/utils/phpcheck.php | 13 + LICENSE | 21 + README.md | 54 ++ app/app.php | 7 + app/bootstrap.php | 5 + app/config.php | 12 + app/console | 7 + app/hacks.php | 5 + app/loader.php | 16 + app/parameters.php | 22 + app/routes/dashboard.php | 10 + app/routes/web.php | 20 + app/settings.php | 0 app/src/Api/Users/DELETE.php | 23 + app/src/Api/Users/Friends/Relationship.php | 13 + app/src/Api/Users/Friends/_GET.php | 10 + app/src/Api/Users/GET.php | 16 + app/src/Api/Users/PATCH.php | 16 + app/src/Api/Users/Resource.php | 18 + app/src/Api/Users/_GET.php | 12 + app/src/Api/Users/_POST.php | 20 + app/src/Controller.php | 13 + app/src/Controllers/Cache.php | 29 + app/src/Controllers/Dashboard.php | 14 + app/src/Controllers/Home.php | 32 + app/src/Controllers/Index.php | 34 + app/src/Controllers/PostComments.php | 23 + app/src/Middlewares/RoleAdmin.php | 32 + app/src/Middlewares/RoleBanned.php | 31 + app/src/User.php | 66 ++ app/translations/traducciones po.txt | 0 composer.json | 31 + content/APP GENERATED CONTENT.txt | 0 content/images/system/fondos, covers, etc.txt | 0 .../Contenido de usuario bajo FOLDER ID.txt | 0 extend/user/USER CUSTOMIZATION.txt | 0 index.php | 15 + phpstan.neon | 13 + 192 files changed, 14832 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 .php-cs create mode 100644 Chevereto-Chevere/LICENSE create mode 100644 Chevereto-Chevere/bootstrap.php create mode 100644 Chevereto-Chevere/composer.json create mode 100644 Chevereto-Chevere/phpunit.php create mode 100644 Chevereto-Chevere/phpunit.xml create mode 100644 Chevereto-Chevere/resources/functions/dump.php create mode 100644 Chevereto-Chevere/src/Api/Api.php create mode 100644 Chevereto-Chevere/src/Api/Endpoint.php create mode 100644 Chevereto-Chevere/src/Api/Maker.php create mode 100644 Chevereto-Chevere/src/Api/src/FilterIterator.php create mode 100644 Chevereto-Chevere/src/App/App.php create mode 100644 Chevereto-Chevere/src/App/Checkout.php create mode 100644 Chevereto-Chevere/src/App/Loader.php create mode 100644 Chevereto-Chevere/src/App/Parameters.php create mode 100644 Chevereto-Chevere/src/ArrayFile/ArrayFile.php create mode 100644 Chevereto-Chevere/src/ArrayFile/ArrayFileCallback.php create mode 100644 Chevereto-Chevere/src/Cache/Cache.php create mode 100644 Chevereto-Chevere/src/CallableWrap.php create mode 100644 Chevereto-Chevere/src/Console/Cli.php create mode 100644 Chevereto-Chevere/src/Console/Command.php create mode 100644 Chevereto-Chevere/src/Console/Commands/BuildCommand.php create mode 100644 Chevereto-Chevere/src/Console/Commands/InspectCommand.php create mode 100644 Chevereto-Chevere/src/Console/Commands/RequestCommand.php create mode 100644 Chevereto-Chevere/src/Console/Commands/RunCommand.php create mode 100644 Chevereto-Chevere/src/Console/Console.php create mode 100644 Chevereto-Chevere/src/Console/SymfonyCommand.php create mode 100644 Chevereto-Chevere/src/Contracts/Api/ApiContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Api/MakerContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Api/src/EndpointContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Api/src/FilterIteratorContract.php create mode 100644 Chevereto-Chevere/src/Contracts/App/AppContract.php create mode 100644 Chevereto-Chevere/src/Contracts/App/CheckoutContract.php create mode 100644 Chevereto-Chevere/src/Contracts/App/LoaderContract.php create mode 100644 Chevereto-Chevere/src/Contracts/App/ParametersContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Console/CliContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Console/CommandContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Console/ConsoleContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Console/SymfonyCommandContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Controller/ArgumentsWrapContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Controller/ControllerContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Controller/InspectContract.php create mode 100644 Chevereto-Chevere/src/Contracts/DataContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Http/MethodContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Http/MethodsContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Http/ResponseContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Http/Symfony/RequestContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Http/Symfony/ResponseContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Render/RenderContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Route/PathValidateContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Route/RouteContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Route/WildcardsContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Router/ResolverContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Router/RouterContract.php create mode 100644 Chevereto-Chevere/src/Contracts/Runtime/RuntimeSetContract.php create mode 100644 Chevereto-Chevere/src/Contracts/ToArrayContract.php create mode 100644 Chevereto-Chevere/src/Controller/ArgumentsWrap.php create mode 100644 Chevereto-Chevere/src/Controller/Controller.php create mode 100644 Chevereto-Chevere/src/Controller/Inspect.php create mode 100644 Chevereto-Chevere/src/Controller/Relationship.php create mode 100644 Chevereto-Chevere/src/Controller/Resource.php create mode 100644 Chevereto-Chevere/src/Controllers/Api/GetController.php create mode 100644 Chevereto-Chevere/src/Controllers/Api/HeadController.php create mode 100644 Chevereto-Chevere/src/Controllers/Api/OptionsController.php create mode 100644 Chevereto-Chevere/src/Controllers/HeadController.php create mode 100644 Chevereto-Chevere/src/Data/Data.php create mode 100644 Chevereto-Chevere/src/Data/Traits/DataAccessTrait.php create mode 100644 Chevereto-Chevere/src/Data/Traits/DataKeyTrait.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/ErrorHandler.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/ExceptionHandler.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/Formatter.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/Output.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/Stack.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/Style.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/Template.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/TemplatedStrings.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/TraceEntry.php create mode 100644 Chevereto-Chevere/src/ExceptionHandler/src/Wrap.php create mode 100644 Chevereto-Chevere/src/File.php create mode 100644 Chevereto-Chevere/src/FileReturn/FileReturn.php create mode 100644 Chevereto-Chevere/src/FromString.php create mode 100644 Chevereto-Chevere/src/Handler.php create mode 100644 Chevereto-Chevere/src/Hooking/Hook.php create mode 100644 Chevereto-Chevere/src/Hooking/Hookable.php create mode 100644 Chevereto-Chevere/src/Http/Http.php create mode 100644 Chevereto-Chevere/src/Http/Method.php create mode 100644 Chevereto-Chevere/src/Http/Methods.php create mode 100644 Chevereto-Chevere/src/Http/Request.php create mode 100644 Chevereto-Chevere/src/Http/Request/RequestException.php create mode 100644 Chevereto-Chevere/src/Http/Response.php create mode 100644 Chevereto-Chevere/src/Interfaces/ControllerRelationshipInterface.php create mode 100644 Chevereto-Chevere/src/Interfaces/ControllerResourceInterface.php create mode 100644 Chevereto-Chevere/src/Interfaces/CreateFromString.php create mode 100644 Chevereto-Chevere/src/Interfaces/HandlerInterface.php create mode 100644 Chevereto-Chevere/src/Interfaces/MiddlewareInterface.php create mode 100644 Chevereto-Chevere/src/Interfaces/PrintableInterface.php create mode 100644 Chevereto-Chevere/src/Interfaces/RenderableInterface.php create mode 100644 Chevereto-Chevere/src/Json.php create mode 100644 Chevereto-Chevere/src/JsonApi/Data.php create mode 100644 Chevereto-Chevere/src/JsonApi/JsonApi.php create mode 100644 Chevereto-Chevere/src/JsonApi/Objects/Error.php create mode 100644 Chevereto-Chevere/src/JsonApi/Objects/JsonApi.php create mode 100644 Chevereto-Chevere/src/JsonApi/Objects/Links.php create mode 100644 Chevereto-Chevere/src/JsonApi/Objects/Meta.php create mode 100644 Chevereto-Chevere/src/JsonApi/Objects/Resource.php create mode 100644 Chevereto-Chevere/src/Log.php create mode 100644 Chevereto-Chevere/src/Message.php create mode 100644 Chevereto-Chevere/src/Path/Path.php create mode 100644 Chevereto-Chevere/src/Path/PathHandle.php create mode 100644 Chevereto-Chevere/src/Route/PathValidate.php create mode 100644 Chevereto-Chevere/src/Route/Route.php create mode 100644 Chevereto-Chevere/src/Route/Set.php create mode 100644 Chevereto-Chevere/src/Route/Wildcard.php create mode 100644 Chevereto-Chevere/src/Router/Exception/RouteNotFoundException.php create mode 100644 Chevereto-Chevere/src/Router/Maker.php create mode 100644 Chevereto-Chevere/src/Router/Resolver.php create mode 100644 Chevereto-Chevere/src/Router/Router.php create mode 100644 Chevereto-Chevere/src/Runtime/Runtime.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDebug.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDefaultCharset.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetErrorHandler.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetExceptionHandler.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetLocale.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetPrecision.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetTimeZone.php create mode 100644 Chevereto-Chevere/src/Runtime/Sets/RuntimeSetUriScheme.php create mode 100644 Chevereto-Chevere/src/Runtime/Traits/RuntimeSet.php create mode 100644 Chevereto-Chevere/src/Stopwatch.php create mode 100644 Chevereto-Chevere/src/Traits/CallableTrait.php create mode 100644 Chevereto-Chevere/src/Traits/HookableTrait.php create mode 100644 Chevereto-Chevere/src/Traits/PrintableTrait.php create mode 100644 Chevereto-Chevere/src/Type.php create mode 100644 Chevereto-Chevere/src/Utility/Arr.php create mode 100644 Chevereto-Chevere/src/Utility/Benchmark.php create mode 100644 Chevereto-Chevere/src/Utility/Bytes.php create mode 100644 Chevereto-Chevere/src/Utility/Color.php create mode 100644 Chevereto-Chevere/src/Utility/DateTime.php create mode 100644 Chevereto-Chevere/src/Utility/Number.php create mode 100644 Chevereto-Chevere/src/Utility/Random.php create mode 100644 Chevereto-Chevere/src/Utility/Str.php create mode 100644 Chevereto-Chevere/src/Validate.php create mode 100644 Chevereto-Chevere/src/VarDump/ConsoleVarDump.php create mode 100644 Chevereto-Chevere/src/VarDump/Dumper.php create mode 100644 Chevereto-Chevere/src/VarDump/HtmlVarDump.php create mode 100644 Chevereto-Chevere/src/VarDump/PlainVarDump.php create mode 100644 Chevereto-Chevere/src/VarDump/VarDump.php create mode 100644 Chevereto-Chevere/src/VarDump/VarDumpAbstract.php create mode 100644 Chevereto-Chevere/src/VarDump/src/Pallete.php create mode 100644 Chevereto-Chevere/src/VarDump/src/Template.php create mode 100644 Chevereto-Chevere/src/VarDump/src/Wrapper.php create mode 100644 Chevereto-Chevere/tests/Api/MakerTest.php create mode 100644 Chevereto-Chevere/utils/phpcheck.php create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/app.php create mode 100644 app/bootstrap.php create mode 100644 app/config.php create mode 100644 app/console create mode 100644 app/hacks.php create mode 100644 app/loader.php create mode 100644 app/parameters.php create mode 100644 app/routes/dashboard.php create mode 100644 app/routes/web.php create mode 100644 app/settings.php create mode 100644 app/src/Api/Users/DELETE.php create mode 100644 app/src/Api/Users/Friends/Relationship.php create mode 100644 app/src/Api/Users/Friends/_GET.php create mode 100644 app/src/Api/Users/GET.php create mode 100644 app/src/Api/Users/PATCH.php create mode 100644 app/src/Api/Users/Resource.php create mode 100644 app/src/Api/Users/_GET.php create mode 100644 app/src/Api/Users/_POST.php create mode 100644 app/src/Controller.php create mode 100644 app/src/Controllers/Cache.php create mode 100644 app/src/Controllers/Dashboard.php create mode 100644 app/src/Controllers/Home.php create mode 100644 app/src/Controllers/Index.php create mode 100644 app/src/Controllers/PostComments.php create mode 100644 app/src/Middlewares/RoleAdmin.php create mode 100644 app/src/Middlewares/RoleBanned.php create mode 100644 app/src/User.php create mode 100644 app/translations/traducciones po.txt create mode 100644 composer.json create mode 100644 content/APP GENERATED CONTENT.txt create mode 100644 content/images/system/fondos, covers, etc.txt create mode 100644 content/images/users/Contenido de usuario bajo FOLDER ID.txt create mode 100644 extend/user/USER CUSTOMIZATION.txt create mode 100644 index.php create mode 100644 phpstan.neon diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..dfe077042 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..988245924 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +_trash/ +_reports/ +vendor/ +docs/ +app/var/ +app/cache/ +Chevereto-Chevere/logs/ +var/ +composer.lock +.vscode/ +.phpunit.result.cache +app/build +/devnotes/ \ No newline at end of file diff --git a/.htaccess b/.htaccess new file mode 100644 index 000000000..6ffa9666c --- /dev/null +++ b/.htaccess @@ -0,0 +1,23 @@ +# Disable server signature +ServerSignature Off + +# Disable directory listing (-indexes), Multiviews (-MultiViews) and enable Follow system links (+FollowSymLinks) +Options -Indexes +Options -MultiViews +Options +FollowSymLinks + + + + RewriteEngine On + + # If you have problems with the rewrite rules remove the "#" from the following RewriteBase line + # You will also have to change the path to reflect the path to your Chevereto installation + # If you are using alias is most likely that you will need this. + RewriteBase /Core + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} !\.(css|js|html|htm|rtf|rtx|svg|svgz|txt|xsd|xsl|xml|asf|asx|wax|wmv|wmx|avi|bmp|class|divx|doc|docx|exe|gif|gz|gzip|ico|jpe?g|jpe|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|mpp|odb|odc|odf|odg|odp|ods|odt|ogg|pdf|png|pot|pps|ppt|pptx|ra|ram|swf|tar|tif|tiff|wav|wma|wri|xla|xls|xlsx|xlt|xlw|zip)$ [NC] + RewriteRule . index.php [L] + + \ No newline at end of file diff --git a/.php-cs b/.php-cs new file mode 100644 index 000000000..355eb10ff --- /dev/null +++ b/.php-cs @@ -0,0 +1,108 @@ + + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + +return PhpCsFixer\Config::create() + ->setRules(array( + '@PSR2' => true, + '@Symfony' => true, + 'yoda_style' => true, + 'array_indentation' => true, + 'array_syntax' => array('syntax' => 'short'), + 'combine_consecutive_unsets' => true, + 'method_separation' => true, + 'no_multiline_whitespace_before_semicolons' => true, + 'single_quote' => true, + + 'binary_operator_spaces' => array( + 'align_double_arrow' => false, + 'align_equals' => false, + ), + // 'blank_line_after_opening_tag' => true, + // 'blank_line_before_return' => true, + 'braces' => array( + 'allow_single_line_closure' => true, + ), + // 'cast_spaces' => true, + // 'class_definition' => array('singleLine' => true), + 'concat_space' => array('spacing' => 'one'), + 'declare_equal_normalize' => true, + 'function_typehint_space' => true, + 'hash_to_slash_comment' => true, + 'include' => true, + 'lowercase_cast' => true, + // 'native_function_casing' => true, + // 'new_with_braces' => true, + // 'no_blank_lines_after_class_opening' => true, + // 'no_blank_lines_after_phpdoc' => true, + // 'no_empty_comment' => true, + // 'no_empty_phpdoc' => true, + // 'no_empty_statement' => true, + 'no_extra_consecutive_blank_lines' => array( + 'curly_brace_block', + 'extra', + 'parenthesis_brace_block', + 'square_brace_block', + 'throw', + 'use', + ), + // 'no_leading_import_slash' => true, + // 'no_leading_namespace_whitespace' => true, + // 'no_mixed_echo_print' => array('use' => 'echo'), + 'no_multiline_whitespace_around_double_arrow' => true, + // 'no_short_bool_cast' => true, + // 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + // 'no_trailing_comma_in_list_call' => true, + // 'no_trailing_comma_in_singleline_array' => true, + // 'no_unneeded_control_parentheses' => true, + // 'no_unused_imports' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + // 'normalize_index_brace' => true, + 'object_operator_without_whitespace' => true, + // 'php_unit_fqcn_annotation' => true, + // 'phpdoc_align' => true, + // 'phpdoc_annotation_without_dot' => true, + // 'phpdoc_indent' => true, + // 'phpdoc_inline_tag' => true, + // 'phpdoc_no_access' => true, + // 'phpdoc_no_alias_tag' => true, + // 'phpdoc_no_empty_return' => true, + // 'phpdoc_no_package' => true, + // 'phpdoc_no_useless_inheritdoc' => true, + // 'phpdoc_return_self_reference' => true, + // 'phpdoc_scalar' => true, + // 'phpdoc_separation' => true, + // 'phpdoc_single_line_var_spacing' => true, + // 'phpdoc_summary' => true, + // 'phpdoc_to_comment' => true, + // 'phpdoc_trim' => true, + // 'phpdoc_types' => true, + // 'phpdoc_var_without_name' => true, + // 'pre_increment' => true, + // 'return_type_declaration' => true, + // 'self_accessor' => true, + // 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + // 'single_class_element_per_statement' => true, + // 'space_after_semicolon' => true, + // 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + // 'trailing_comma_in_multiline_array' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + 'header_comment' => ['header' => $header], + 'declare_strict_types' => true, + )) + //->setIndent("\t") + ->setLineEnding("\n") +; diff --git a/Chevereto-Chevere/LICENSE b/Chevereto-Chevere/LICENSE new file mode 100644 index 000000000..58d2abd1d --- /dev/null +++ b/Chevereto-Chevere/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) Rodolfo Berrios + +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/Chevereto-Chevere/bootstrap.php b/Chevereto-Chevere/bootstrap.php new file mode 100644 index 000000000..549c76f04 --- /dev/null +++ b/Chevereto-Chevere/bootstrap.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +use Chevere\App\App; +use Chevere\App\Loader; +use Chevere\Console\Console; +use Chevere\Runtime\Runtime; +use Chevere\Runtime\Sets\RuntimeSetDebug; +use Chevere\Runtime\Sets\RuntimeSetDefaultCharset; +use Chevere\Runtime\Sets\RuntimeSetPrecision; +use Chevere\Runtime\Sets\RuntimeSetTimeZone; +use Chevere\Runtime\Sets\RuntimeSetUriScheme; +use Chevere\Runtime\Sets\RuntimeSetLocale; +use Chevere\Runtime\Sets\RuntimeSetErrorHandler; +use Chevere\Runtime\Sets\RuntimeSetExceptionHandler; + +/** DEV_MODE true rebuild the App on every load */ +define('Chevere\DEV_MODE', false); + +/* + * Assuming that this file has been loaded from /app/bootstrap.php + */ + +define('Chevere\BOOTSTRAPPER', debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file']); + +/* Root path containing /app */ +define('Chevere\ROOT_PATH', rtrim(str_replace('\\', '/', dirname(BOOTSTRAPPER, 2)), '/') . '/'); + +/* + * Chevere\PATH + * Relative path to Core, usually 'vendor/chevereto/chevereto-core' + */ +define('Chevere\PATH', rtrim(str_replace(ROOT_PATH, null, str_replace('\\', '/', __DIR__)), '/') . '/'); + +/* Relative path to app, usually 'app' */ +define('Chevere\APP_PATH_RELATIVE', basename(dirname(BOOTSTRAPPER)) . '/'); +define('Chevere\APP_PATH', ROOT_PATH . APP_PATH_RELATIVE); + +if ('cli' == php_sapi_name()) { + Console::init(); //10ms +} + +define('Chevere\CLI', Console::isAvailable()); + +// $sw = new Stopwatch(); +Loader::setDefaultRuntime( + new Runtime( + new RuntimeSetDebug('1'), // 0.2ms + new RuntimeSetErrorHandler('Chevere\ExceptionHandler\ErrorHandler::error'), // 0.9ms + new RuntimeSetExceptionHandler('Chevere\ExceptionHandler\ExceptionHandler::exception'), // 0.5ms + new RuntimeSetLocale('en_US.UTF8'), // 0.2ms + new RuntimeSetDefaultCharset('utf-8'), // 0.2ms + new RuntimeSetPrecision('16'), // 0.2ms + new RuntimeSetUriScheme('https'), // 0.2ms + new RuntimeSetTimeZone('UTC') // 1.85 + ) +); // 0.6ms wrapper + +// $sw->stop(); +// dd($sw->records(), 'BOOTSTRAP'); + + // ->addFile(App::FILEHANDLE_CONFIG) diff --git a/Chevereto-Chevere/composer.json b/Chevereto-Chevere/composer.json new file mode 100644 index 000000000..17db08020 --- /dev/null +++ b/Chevereto-Chevere/composer.json @@ -0,0 +1,30 @@ +{ + "name": "chevereto/chevere", + "description": "Chevereto Framework", + "homepage": "http://github.com/chevereto/chevere", + "type": "library", + "license": "MIT", + "authors": [{ + "name": "Rodolfo Berrios", + "email": "inbox@rodolfoberrios.com", + "homepage": "http://rodolfoberrios.com" + }], + "require": { + "php": ">=7.2", + "psr/simple-cache": "~1.0", + "monolog/monolog": "~1.24", + "symfony/http-foundation": "~4.2", + "symfony/console": "~4.2", + "jakub-onderka/php-console-color": "~0.2", + "roave/better-reflection": "~3.2" + }, + "require-dev": { + "phpunit/phpunit": "^8" + }, + "autoload": { + "files": ["utils/phpcheck.php", "resources/functions/dump.php"], + "psr-4": { + "Chevere\\": "src/" + } + } +} \ No newline at end of file diff --git a/Chevereto-Chevere/phpunit.php b/Chevereto-Chevere/phpunit.php new file mode 100644 index 000000000..e8b56da75 --- /dev/null +++ b/Chevereto-Chevere/phpunit.php @@ -0,0 +1,11 @@ + + + + + ./tests + + + \ No newline at end of file diff --git a/Chevereto-Chevere/resources/functions/dump.php b/Chevereto-Chevere/resources/functions/dump.php new file mode 100644 index 000000000..e162941f5 --- /dev/null +++ b/Chevereto-Chevere/resources/functions/dump.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Chevere\VarDump\Dumper; + +/** + * Dumps information about one or more variables. + */ +function dump(...$vars) +{ + Dumper::dump(...$vars); +} +/** + * Dumps information about one or more variables and die(). + */ +function dd(...$vars) +{ + Dumper::dd(...$vars); +} diff --git a/Chevereto-Chevere/src/Api/Api.php b/Chevereto-Chevere/src/Api/Api.php new file mode 100644 index 000000000..8c4ed090b --- /dev/null +++ b/Chevereto-Chevere/src/Api/Api.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Api; + +use Chevere\Cache\Cache; +use LogicException; +use Chevere\Message; +use Chevere\Contracts\Api\ApiContract; +use Chevere\FileReturn\FileReturn; +use Chevere\Path\PathHandle; +use Chevere\Stopwatch; + +/** + * Api provides a static method to read the exposed API inside the app runtime. + */ +final class Api implements ApiContract +{ + /** @var string Prefix used for endpoints without a defined resource (/endpoint) */ + const METHOD_ROOT_PREFIX = '_'; + + /** @var array */ + private static $api; + + public function __construct(Maker $maker = null) + { + if (isset($maker)) { + self::$api = $maker->api(); + $maker->setCache(); + } else { + $cache = new Cache('api'); + self::$api = $cache->get('api')->raw(); + } + } + + public function get(): array + { + return self::$api; + } + + public static function endpoint(string $uriKey): array + { + $key = self::endpointKey($uriKey); + if ($key) { + $subKey = ltrim($uriKey, '/') == $key ? '' : $uriKey; + + return self::$api[$key][$subKey]; + } + + throw new LogicException( + (new Message('No endpoint defined for the %s URI.')) + ->code('%s', $uriKey) + ->toString() + ); + } + + /** + * @return string The the endpoint basename for the given URI. + */ + public static function endpointKey(string $uri): string + { + $endpoint = ltrim($uri, '/'); + $base = strtok($endpoint, '/'); + + if (!isset(self::$api[$base])) { + throw new LogicException( + (new Message('No API endpoint key for the %s URI.')) + ->code('%s', $uri) + ->toString() + ); + } + + return $base; + } +} diff --git a/Chevereto-Chevere/src/Api/Endpoint.php b/Chevereto-Chevere/src/Api/Endpoint.php new file mode 100644 index 000000000..c9a970581 --- /dev/null +++ b/Chevereto-Chevere/src/Api/Endpoint.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Api; + +use Chevere\Controllers\Api\OptionsController; +use Chevere\Controllers\Api\HeadController; +use Chevere\Contracts\Api\src\EndpointContract; +use Chevere\Contracts\Http\MethodsContract; +use Chevere\Http\Method; + +final class Endpoint implements EndpointContract +{ + /** @var array */ + private $array; + + /** @var MethodsContract */ + private $methods; + + public function __construct(MethodsContract $methods) + { + $this->array = []; + $this->methods = $methods; + $this->fillEndpointOptions(); + $this->autofillMissingOptionsHead(); + } + + public function methods(): MethodsContract + { + return $this->methods; + } + + public function toArray(): array + { + return $this->array; + } + + public function setResource(array $resource): void + { + $this->array['resource'] = $resource; + } + + private function fillEndpointOptions(): void + { + foreach ($this->methods as $method) { + $httpMethod = $method->method(); + $controllerClassName = $method->controller(); + $httpMethodOptions = []; + $httpMethodOptions['description'] = $controllerClassName::description(); + $controllerParameters = $controllerClassName::parameters(); + if (isset($controllerParameters)) { + $httpMethodOptions['parameters'] = $controllerParameters; + } + $this->array['OPTIONS'][$httpMethod] = $httpMethodOptions; + } + } + + private function autofillMissingOptionsHead(): void + { + foreach ([ + 'OPTIONS' => [ + OptionsController::class, [ + 'description' => OptionsController::description(), + ], + ], + 'HEAD' => [ + HeadController::class, [ + 'description' => HeadController::description(), + ], + ], + ] as $k => $v) { + if (!$this->methods->has($k)) { + $this->methods->add(new Method($k, $v[0])); + $this->array['OPTIONS'][$k] = $v[1]; + } + } + } +} diff --git a/Chevereto-Chevere/src/Api/Maker.php b/Chevereto-Chevere/src/Api/Maker.php new file mode 100644 index 000000000..f4fcda274 --- /dev/null +++ b/Chevereto-Chevere/src/Api/Maker.php @@ -0,0 +1,231 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Api; + +use OuterIterator; +use LogicException; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use Throwable; +use const Chevere\APP_PATH_RELATIVE; +use Chevere\Route\Route; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Http\Method; +use Chevere\Http\Methods; +use Chevere\Message; +use Chevere\Path\Path; +use Chevere\Path\PathHandle; +use Chevere\File; +use Chevere\Utility\Str; +use Chevere\Controller\Inspect; +use Chevere\Api\src\FilterIterator; +use Chevere\Cache\Cache; +use Chevere\Router\Maker as RouterMaker; + +final class Maker +{ + /** @var array Route mapping [route => [http_method => Controller]]] */ + private $routesMap; + + /** @var array Maps [endpoint => (array) resource [regex =>, description =>,]] (for wildcard routes) */ + private $resourcesMap; + + /** @var OuterIterator */ + private $recursiveIterator; + + /** @var array Endpoint API properties */ + private $api; + + /** @var RouterMaker The injected Router, needed to add Routes to the injector instance */ + private $routerMaker; + + /** @var array Contains registered API paths via register() */ + private $registered; + + /** @var string The API basepath, like 'api' */ + private $basePath; + + /** @var RouteContract */ + private $route; + + /** @var string Target API directory (absolute) */ + private $path; + + /** @var Cache */ + private $cache; + + public function __construct(RouterMaker $router) + { + $this->routerMaker = $router; + } + + public function register(PathHandle $pathHandle, Endpoint $endpoint): void + { + $this->path = $pathHandle->path(); + $this->validateNoDuplicates(); + $this->validatePath(); + $this->basePath = strtolower(basename($this->path)); + $this->routesMap = []; + $this->resourcesMap = []; + // $this->controllersMap = []; + $this->api = []; + + $iterator = new RecursiveDirectoryIterator($this->path, RecursiveDirectoryIterator::SKIP_DOTS); + $filter = new FilterIterator($iterator); + $filter->generateAcceptedFilenames(Method::ACCEPT_METHODS, Api::METHOD_ROOT_PREFIX); + $this->recursiveIterator = new RecursiveIteratorIterator($filter); + $this->validateRecursiveIterator(); + $this->processRecursiveIterator(); + + $this->processRoutesMap(); + + $path = '/' . $this->basePath; + $this->api[$this->basePath][''] = $endpoint->toArray(); + + $route = new Route($path); + $route->setMethods($endpoint->methods())->setId($this->basePath); + $this->routerMaker->addRoute($route, $this->basePath); + + $this->registered[$this->basePath] = true; + ksort($this->api); + } + + public function api(): array + { + return $this->api; + } + + public function setCache(): void + { + $this->cache = new Cache('api'); + $this->cache->put('api', $this->api); + } + + public function cache(): Cache + { + return $this->cache; + } + + private function validateRecursiveIterator(): void + { + try { + $count = iterator_count($this->recursiveIterator); + } catch (Throwable $e) { + throw new LogicException($e->getMessage()); + } + if ($count == 0) { + throw new LogicException( + (new Message('No API methods found in the %s path.')) + ->code('%s', $this->path) + ->toString() + ); + } + } + + private function validateNoDuplicates(): void + { + if (isset($this->registered[$this->path])) { + throw new LogicException( + (new Message('Path identified by %s has been already bound.')) + ->code('%s', $this->path) + ->toString() + ); + } + } + + private function validatePath(): void + { + if (!File::exists($this->path)) { + throw new LogicException( + (new Message("Directory %s doesn't exists.")) + ->code('%s', $this->path) + ->toString() + ); + } + if (!is_readable($this->path)) { + throw new LogicException( + (new Message('Directory %s is not readable.')) + ->code('%s', $this->path) + ->toString() + ); + } + } + + private function processRecursiveIterator(): void + { + foreach ($this->recursiveIterator as $filename) { + $filepathAbsolute = Str::forwardSlashes((string) $filename); + $className = $this->getClassNameFromFilepath($filepathAbsolute); + $inspected = new Inspect($className); + // $this->controllersMap[$className] = $inspected; + $pathComponent = $inspected->pathComponent; + if ($inspected->useResource) { + $this->resourcesMap[$pathComponent] = $inspected->resourcesFromString; + /* + * For relationships we need to create the /endpoint/{id}/relationships/relation URLs. + * @see https://jsonapi.org/recommendations/ + */ + if ($inspected->isRelatedResource) { + $this->routesMap[$inspected->relationshipPathComponent]['GET'] = $inspected->relationship; + } + } + $this->routesMap[$pathComponent][$inspected->httpMethod] = $inspected->className; + } + ksort($this->routesMap); + } + + private function processRoutesMap(): void + { + foreach ($this->routesMap as $pathComponent => $httpMethods) { + $methodsArray = []; + foreach ($httpMethods as $httpMethod => $controller) { + $methodsArray[] = new Method($httpMethod, $controller); + } + $methods = new Methods(...$methodsArray); + $endpoint = new Endpoint($methods); + /** @var string Full qualified route key for $pathComponent like /api/users/{user} */ + $endpointRouteKey = Str::ltail($pathComponent, '/'); + + $this->route = (new Route($endpointRouteKey)) + ->setId($pathComponent) + ->setMethods($methods); + $resource = $this->resourcesMap[$pathComponent] ?? null; + if (isset($resource)) { + foreach ($resource as $wildcardKey => $resourceMeta) { + $this->route->setWhere($wildcardKey, $resourceMeta['regex']); + } + $endpoint->setResource($resource); + } + $this->routerMaker->addRoute($this->route, $this->basePath); + $this->api[$this->basePath][$pathComponent] = $endpoint->toArray(); + } + ksort($this->api); + } + + /** + * Returns the namespaced class name for the specified filepath. + * + * @param string $filepath the class filepath + * + * @return string the class name detected according autoloading standard (PSR-4) + */ + private function getClassNameFromFilepath(string $filepath): string + { + $filepathRelative = Path::relative($filepath); + $filepathNoExt = Str::replaceLast('.php', '', $filepathRelative); + $filepathReplaceNS = Str::replaceFirst(APP_PATH_RELATIVE . 'src/', 'App\\', $filepathNoExt); + + return str_replace('/', '\\', $filepathReplaceNS); + } +} diff --git a/Chevereto-Chevere/src/Api/src/FilterIterator.php b/Chevereto-Chevere/src/Api/src/FilterIterator.php new file mode 100644 index 000000000..be56d1f84 --- /dev/null +++ b/Chevereto-Chevere/src/Api/src/FilterIterator.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Api\src; + +use RecursiveFilterIterator; +use Chevere\Contracts\Api\src\FilterIteratorContract; + +/** + * Provides filtering for the Api register process (directory scan). + */ +final class FilterIterator extends RecursiveFilterIterator implements FilterIteratorContract +{ + /** @var array Accepted files array [GET.php, _GET.php, POST.php, ...] */ + private $acceptFilenames; + + /** + * {@inheritdoc} + */ + public function generateAcceptedFilenames(array $methods, string $methodPrefix): FilterIteratorContract + { + foreach ($methods as $v) { + $this->acceptFilenames[] = $v.'.php'; + $this->acceptFilenames[] = $methodPrefix.$v.'.php'; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getChildren() + { + $children = parent::getChildren(); + $children->acceptFilenames = $this->acceptFilenames; + + return $children; + } + + /** + * {@inheritdoc} + */ + public function accept(): bool + { + return $this->hasChildren() || in_array($this->current()->getFilename(), $this->acceptFilenames); + } +} diff --git a/Chevereto-Chevere/src/App/App.php b/Chevereto-Chevere/src/App/App.php new file mode 100644 index 000000000..37d555b09 --- /dev/null +++ b/Chevereto-Chevere/src/App/App.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\App; + +use Chevere\Contracts\Api\ApiContract; +use LogicException; + +use const Chevere\APP_PATH; +use Chevere\Message; +use Chevere\Contracts\App\AppContract; +use Chevere\Controller\ArgumentsWrap; +use Chevere\Http\Response; +use Chevere\Contracts\Controller\ControllerContract; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Contracts\Router\RouterContract; +use Chevere\Handler; +use Chevere\Http\Request; + +/** + * The app container. + */ +final class App implements AppContract +{ + const BUILD_FILEPATH = APP_PATH . 'build'; + const NAMESPACES = ['App', 'Chevere']; + const APP = 'app'; + const FILEHANDLE_CONFIG = ':config'; + const FILEHANDLE_PARAMETERS = ':parameters'; + const FILEHANDLE_HACKS = ':hacks'; + + /** @var ApiContract */ + private $api; + + /** @var Request */ + private $request; + + /** @var Response */ + private $response; + + /** @var array String arguments (from request uri, cli) */ + private $arguments; + + /** @var RouteContract */ + private $route; + + /** @var RouterContract */ + private $router; + + public function setApi(ApiContract $api): void + { + $this->api = $api; + } + + public function setRequest(Request $request): void + { + $this->request = $request; + } + + public function setResponse(Response $response): void + { + $this->response = $response; + } + + public function setRoute(RouteContract $route): void + { + $this->route = $route; + } + + public function setRouter(RouterContract $router): void + { + $this->router = $router; + } + + public function setArguments(array $arguments): void + { + $this->arguments = $arguments; + } + + public function api(): ApiContract + { + return $this->api; + } + + public function request(): Request + { + return $this->request; + } + + public function response(): Response + { + return $this->response; + } + + public function route(): RouteContract + { + return $this->route; + } + + public function router(): RouterContract + { + return $this->router; + } + + public function arguments(): array + { + return $this->arguments ?? []; + } + + /** + * {@inheritdoc} + */ + // public function getBuildTime(): ?string + // { + // return File::exists(self::BUILD_FILEPATH) ? (string) file_get_contents(self::BUILD_FILEPATH) : null; + // } + + /** + * {@inheritdoc} + */ + public function run(string $controller): ControllerContract + { + if (!is_subclass_of($controller, ControllerContract::class)) { + throw new LogicException( + (new Message('Controller %s must implement the %c contract.')) + ->code('%s', $controller) + ->code('%i', ControllerContract::class) + ->toString() + ); + } + + $middlewares = $this->route->middlewares(); + if (!empty($middlewares)) { + $handler = new Handler($middlewares, $this); + $handler->runner(); + if ($handler->exception) { + dd($handler->exception->getMessage(), 'Aborted at ' . __FILE__ . ':' . __LINE__); + } + } + + $controller = new $controller($this); + + if (isset($this->arguments)) { + $wrap = new ArgumentsWrap($controller, $this->arguments); + $controllerArguments = $wrap->typedArguments(); + } else { + $controllerArguments = []; + } + + $controller(...$controllerArguments); + + return $controller; + } +} diff --git a/Chevereto-Chevere/src/App/Checkout.php b/Chevereto-Chevere/src/App/Checkout.php new file mode 100644 index 000000000..2c35cb95b --- /dev/null +++ b/Chevereto-Chevere/src/App/Checkout.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\App; + +use RuntimeException; +use Chevere\Message; +use Chevere\Contracts\App\CheckoutContract; + +/** + * ArrayFile provides a object oriented method to interact with array files (return []). + */ +final class Checkout +{ + public function __construct() + { + if (!file_put_contents(App::BUILD_FILEPATH, (string) time())) { + throw new RuntimeException( + (new Message('Unable to checkout to %file%')) + ->code('%file', App::BUILD_FILEPATH) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/App/Loader.php b/Chevereto-Chevere/src/App/Loader.php new file mode 100644 index 000000000..50779fe0e --- /dev/null +++ b/Chevereto-Chevere/src/App/Loader.php @@ -0,0 +1,304 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\App; + +use LogicException; +use RuntimeException; + +use const Chevere\CLI; +use const Chevere\DEV_MODE; +use Chevere\ArrayFile\ArrayFile; +use Chevere\ArrayFile\ArrayFileCallback; +use Chevere\Path\PathHandle; +use Chevere\Api\Api; +use Chevere\Api\Maker as ApiMaker; +use Chevere\Console\Console; +use Chevere\Http\Request; +use Chevere\Http\Response; +use Chevere\Router\Maker as RouterMaker; +use Chevere\Runtime\Runtime; +use Chevere\Contracts\App\AppContract; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Controllers\Api\HeadController; +use Chevere\Controllers\Api\OptionsController; +use Chevere\Controllers\Api\GetController; +use Chevere\Http\Method; +use Chevere\Http\Methods; +use Chevere\Api\Endpoint; +use Chevere\Contracts\App\LoaderContract; +use Chevere\Contracts\Render\RenderContract; +use Chevere\Contracts\Router\RouterContract; +use Chevere\File; +use Chevere\Message; +use Chevere\Router\Exception\RouteNotFoundException; +use Chevere\Router\Router; +use Chevere\Type; + +final class Loader implements LoaderContract +{ + const CACHED = false; + + /** @var Runtime */ + private static $runtime; + + /** @var AppContract */ + public $app; + + /** @var Api */ + private $api; + + /** @var ApiMaker */ + private $apiMaker; + + /** @var string */ + private $controller; + + /** @var Request */ + private $request; + + /** @var RouterContract */ + private $router; + + /** @var RouterMaker */ + private $routerMaker; + + /** @var bool True if run() has been called */ + private $ran; + + /** @var array An array containing the collection of Cache->toArray() data (checksums) */ + private $cacheChecksums; + + /** @var bool True if the console loop ran */ + private $consoleLoop; + + /** @var Parameters */ + private $parameters; + + /** @var array */ + private $arguments; + + public function __construct() + { + Console::bind($this); + + if (!DEV_MODE && !Console::isBuilding() && !File::exists(App::BUILD_FILEPATH)) { + throw new RuntimeException( + (new Message('The application needs to be built before being able to use %className%.')) + ->code('%className%', __CLASS__) + ->toString() + ); + } + + $this->routerMaker = new RouterMaker(); + $this->app = new App(); + $this->app->setResponse(new Response()); + + $this->parameters = new Parameters( + new ArrayFile( + new PathHandle(App::FILEHANDLE_PARAMETERS) + ) + ); + + if (DEV_MODE && !Console::isBuilding()) { + $this->build(); + } + + $this->applyParameters(); + } + + public function build(): void + { + $this->cacheChecksums = []; + if (!empty($this->parameters->api())) { + $this->createApiMaker(new PathHandle($this->parameters->api())); + $this->api = new Api($this->apiMaker); + $this->cacheChecksums = $this->apiMaker->cache()->toArray(); + } + if (!empty($this->parameters->routes())) { + $this->routerMaker->addRoutesArrays($this->parameters->routes()); + $this->router = new Router($this->routerMaker); + $this->cacheChecksums = array_merge($this->routerMaker->cache()->toArray(), $this->cacheChecksums); + } + new Checkout(); + } + + /** + * {@inheritdoc} + */ + public function setController(string $controller): void + { + $this->controller = $controller; + } + + /** + * {@inheritdoc} + */ + public function setArguments(array $arguments): LoaderContract + { + $this->arguments = $arguments; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setRequest(Request $request): void + { + $this->request = $request; + $pathinfo = ltrim($this->request->getPathInfo(), '/'); + $this->request->attributes->set('requestArray', explode('/', $pathinfo)); + $this->app->setRequest($this->request); + } + + public function cacheChecksums(): array + { + return $this->cacheChecksums; + } + + /** + * {@inheritdoc} + */ + public function run(): void + { + $this->handleConsole(); + $this->handleRequest(); + $this->setRan(); + + if (!isset($this->controller)) { + $this->processResolveCallable($this->request->getPathInfo()); + } + + if (!isset($this->controller)) { + throw new RuntimeException('DESCONTROL'); + } + $this->runController($this->controller); + } + + private function handleConsole() + { + if (Console::isAvailable() && !isset($this->consoleLoop)) { + $this->consoleLoop = true; + Console::run(); + } + } + + private function handleRequest() + { + if (!isset($this->request)) { + $this->setRequest(Request::createFromGlobals()); + } + } + + private function setRan() + { + if (isset($this->ran)) { + throw new LogicException( + (new Message('The method %s has been already called.')) + ->code('%s', __METHOD__) + ->toString() + ); + } + $this->ran = true; + } + + /** + * {@inheritdoc} + */ + public static function runtime(): Runtime + { + if (isset(self::$runtime)) { + return self::$runtime; + } + throw new RuntimeException('NO RUNTIME INSTANCE EVERYTHING BURNS!'); + } + + /** + * {@inheritdoc} + */ + public static function request(): Request + { + if (isset(self::$request)) { + return self::$request; + } + throw new RuntimeException('NO REQUEST INSTANCE EVERYTHING BURNS!'); + } + + /** + * {@inheritdoc} + */ + public static function setDefaultRuntime(Runtime $runtime) + { + self::$runtime = $runtime; + } + + private function applyParameters() + { + if (!empty($this->parameters->api()) && !isset($this->api)) { + $this->api = new Api(); + } + // $this->app->setApi($this->api); + if (!empty($this->parameters->routes()) && !isset($this->router)) { + $this->router = new Router(); + } + $this->app->setRouter($this->router); + } + + private function processResolveCallable(string $pathInfo): void + { + try { + $route = $this->router->resolve($pathInfo); + } catch (RouteNotFoundException $e) { + $this->app->response()->setStatusCode(404); + $this->app->response()->setContent('404'); + $this->app->response()->prepare($this->app->request()); + $this->app->response()->send(); + if (CLI) { + throw new RouteNotFoundException($e->getMessage()); + } else { + die(); + } + } + $this->app->setRoute($route); + $this->controller = $this->app->route()->getController($this->request->getMethod()); + $routerArgs = $this->router->arguments(); + if (!isset($this->arguments) && isset($routerArgs)) { + $this->setArguments($routerArgs); + } + } + + private function runController(string $controller): void + { + $this->app->setArguments($this->arguments); + $controller = $this->app->run($controller); + if ($controller instanceof RenderContract) { + $controller->render(); + } else { + $jsonApi = $controller->document(); + $this->app->response()->setJsonContent($jsonApi); + $this->app->response()->prepare($this->app->request()); + $this->app->response()->send(); + } + } + + private function createApiMaker(PathHandle $pathHandle): void + { + $this->apiMaker = new ApiMaker($this->routerMaker); + $methods = new Methods( + new Method('HEAD', HeadController::class), + new Method('OPTIONS', OptionsController::class), + new Method('GET', GetController::class) + ); + $this->apiMaker->register($pathHandle, new Endpoint($methods)); // 41ms no cache + } +} diff --git a/Chevereto-Chevere/src/App/Parameters.php b/Chevereto-Chevere/src/App/Parameters.php new file mode 100644 index 000000000..3b22ab2e0 --- /dev/null +++ b/Chevereto-Chevere/src/App/Parameters.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\App; + +use LogicException; +use Chevere\ArrayFile\ArrayFile; +use Chevere\Message; +use Chevere\Contracts\App\ParametersContract; +use Chevere\Data\Traits\DataKeyTrait; + +final class Parameters implements ParametersContract +{ + use DataKeyTrait; + + const CONFIG_FILES = 'configFiles'; + + /** + * @var string Used to describe the path where App scans for API HTTP Controllers. Target path must be autoloaded. + * + * {@example 'api' => 'src/Api'} + */ + const API = 'api'; + + /** + * @var string Used to describe the array which lists the route files (relative to app). + * + * {@example 'routes' => ['routes:dashboard', 'routes:web',]} + */ + const ROUTES = 'routes'; + + /** + * The keys accepted by this class, with the gettype at right side. + */ + private $keys = [ + // self::CONFIG_FILES => 'array', + self::API => 'string', + self::ROUTES => 'array', + ]; + + /** @var ArrayFile The parameters array used to construct the object */ + private $arrayFile; + + /** @var string */ + private $api; + + /** @var array */ + private $routes; + + public function __construct(ArrayFile $arrayFile) + { + $this->arrayFile = $arrayFile; + $this->validate(); + $array = $this->arrayFile->toArray(); + $this->api = $array[static::API]; + $this->routes = $array[static::ROUTES]; + } + + public function api(): string + { + return $this->api ?? ''; + } + + public function routes(): array + { + return $this->routes ?? []; + } + + private function validate(): void + { + foreach ($this->arrayFile as $key => $val) { + $this->validateKeyExists($key); + $this->validateKeyType($key, $val); + } + } + + /** + * Throws a LogicException if the key doesn't exists in $parameters. + * + * @param string $key The AppParameter key + */ + private function validateKeyExists(string $key): void + { + if (!array_key_exists($key, $this->keys)) { + throw new LogicException( + (new Message('Unrecognized %c key "%s".')) + ->code('%c', __CLASS__) + ->strtr('%s', $key) + ->toString() + ); + } + } + + /** + * Throws a LogicException if the key type doesn't meet the type in $keys. + * + * @param string $key The AppParameter key + */ + private function validateKeyType(string $key, $val): void + { + $gettype = gettype($val); + if ($gettype !== $this->keys[$key]) { + throw new LogicException( + (new Message('Expecting %s type, %t type provided for key %k in %c.')) + ->code('%s', $this->keys[$key]) + ->code('%t', $gettype) + ->code('%k', $key) + ->code('%c', $this->arrayFile->pathHandle()->path()) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/ArrayFile/ArrayFile.php b/Chevereto-Chevere/src/ArrayFile/ArrayFile.php new file mode 100644 index 000000000..3a04edf0c --- /dev/null +++ b/Chevereto-Chevere/src/ArrayFile/ArrayFile.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ArrayFile; + +use ArrayAccess; +use ArrayIterator; +use Chevere\FileReturn\FileReturn; +use LogicException; +use IteratorAggregate; +use Chevere\Message; +use Chevere\Type; +use Chevere\Path\PathHandle; + +// FIXME: Make a client for injecting type. Don't pass type in construct pls. + +/** + * ArrayFile provides a object oriented method to interact with array files (return []). + */ +final class ArrayFile implements IteratorAggregate, ArrayAccess +{ + /** @var array The array returned by the file */ + private $array; + + /** @var PathHandle */ + private $pathHandle; + + /** @var FileReturn */ + private $fileReturn; + + /** @var Type */ + private $type; + + /** + * @param PathHandle $pathHandle Path handle or absolute filepath + * @param Type $type If set, the array members must match the target type, classname or interface + */ + public function __construct(PathHandle $pathHandle, Type $type = null) + { + $this->pathHandle = $pathHandle; + $this->fileReturn = new FileReturn($pathHandle); + $this->fileReturn->setStrict(false); + + try { + $this->validateIsArray(); + $this->array = $this->fileReturn->raw(); + if (null !== $type) { + $this->type = $type; + $this->validate(); + } + } catch (LogicException $e) { + throw new LogicException( + (new Message($e->getMessage())) + ->code('%returnType%', $this->fileReturn->type()) + ->code('%filepath%', $this->pathHandle->path()) + ->code('%members%', $this->type->typeString()) + ->toString() + ); + } + } + + public function offsetSet($offset, $value) + { + $this->array[$offset ?? ''] = $value; + } + + public function offsetExists($offset): bool + { + return isset($this->array[$offset]); + } + + public function offsetGet($offset) + { + return $this->array[$offset] ?? null; + } + + public function offsetUnset($offset) + { + unset($this->array[$offset]); + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->array); + } + + public function pathHandle(): PathHandle + { + return $this->pathHandle; + } + + public function toArray(): array + { + return $this->array ?? []; + } + + private function validateIsArray(): void + { + if ('array' !== $this->fileReturn->type()) { + throw new LogicException('Expecting file %filepath% return type array, %returnType% provided.'); + } + } + + /** + * Validates array content type. + */ + private function validate(): void + { + $validator = $this->type->validator(); + foreach ($this->array as $k => $object) { + if ($validate = $validator($object)) { + if ($this->type->primitive() == 'object') { + $validate = $this->type->validate($object); + } + } + if (!$validate) { + $this->handleInvalidation($k, $object); + } + } + } + + private function handleInvalidation($k, $object): void + { + $type = gettype($object); + if ($type == 'object') { + $type .= ' ' . get_class($object); + } + throw new LogicException( + (new Message('Expecting array containing only %members% members, type %type% found at %filepath% (array key %key%).')) + ->code('%type%', $type) + ->code('%key%', $k) + ->toString() + ); + } +} diff --git a/Chevereto-Chevere/src/ArrayFile/ArrayFileCallback.php b/Chevereto-Chevere/src/ArrayFile/ArrayFileCallback.php new file mode 100644 index 000000000..193fb35c2 --- /dev/null +++ b/Chevereto-Chevere/src/ArrayFile/ArrayFileCallback.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ArrayFile; + +use ArrayIterator; +use IteratorAggregate; + +/** + * Provides a wrapping callback for ArrayFile. + */ +final class ArrayFileCallback implements IteratorAggregate +{ + /** @var ArrayFile */ + private $arrayFile; + + /** @var array */ + private $array; + + public function __construct(ArrayFile $arrayFile, callable $callback) + { + foreach ($arrayFile as $k => $v) { + $callback($k, $v); + } + $this->arrayFile = $arrayFile; + $this->array = $this->arrayFile->toArray(); + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->array); + } + + public function arrayFile(): ArrayFile + { + return $this->arrayFile; + } +} diff --git a/Chevereto-Chevere/src/Cache/Cache.php b/Chevereto-Chevere/src/Cache/Cache.php new file mode 100644 index 000000000..1f85c25d5 --- /dev/null +++ b/Chevereto-Chevere/src/Cache/Cache.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Cache; + +use Chevere\Data\Data; +use InvalidArgumentException; +use Chevere\Message; +use Chevere\File; +use Chevere\FileReturn\FileReturn; +use Chevere\Path\Path; +use Chevere\Path\PathHandle; + +/** + * A simple PHP based cache system. + * + * Using FileReturn, it provides cache by using php files that return a single variable. + * + * cached.php >>> [checksum => , path =>]] containing information about the cache instance */ + private $array; + + public function __construct(string $name) + { + $this->validateKey($name); + $this->name = $name; + $this->baseKey = 'cache/' . $name . ':'; + } + + /** + * Get cache as a FileReturn object + * + * @return FileReturn A FileReturn instance for the cache file. + */ + public function get(string $key): FileReturn + { + return new FileReturn( + new PathHandle($this->getFileIdentifier($key)) + ); + } + + public function exists(string $key): bool + { + $path = Path::fromIdentifier($this->getFileIdentifier($key)); + return File::exists($path); + } + + /** + * Put cache + * + * @param string $key Cache key + * @param mixed $var Anything, but keep it restricted to one-dimension iterables at most. + * + * @return FileReturn A FileReturn instance for the cached file. + */ + public function put(string $key, $var): FileReturn + { + $fileReturn = $this->get($key); + $fileReturn->put($var); + $this->array[$this->name][$key] = [ + 'path' => $fileReturn->path(), + 'checksum' => $fileReturn->checksum(), + ]; + return $fileReturn; + } + + public function remove(string $key): void + { + $fileIdentifier = $this->getFileIdentifier($key); + $path = Path::fromIdentifier($fileIdentifier); + if (!File::exists($path)) { + return; + } + unlink($path); + unset($this->array[$this->name][$key]); + } + + public function toArray(): array + { + return $this->array; + } + + /** + * @return string Cache file path identifier for the given $name + */ + private function getFileIdentifier(string $name): string + { + $this->validateKey($name); + return $this->baseKey . $name; + } + + private function validateKey(string $key): void + { + if (preg_match_all('#[' . static::ILLEGAL_KEY_CHARACTERS . ']#', $key, $matches)) { + $matches = array_unique($matches[0]); + $forbidden = implode(', ', $matches); + throw new InvalidArgumentException( + (new Message('Use of forbidden character %forbidden%.')) + ->code('%forbidden%', $forbidden) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/CallableWrap.php b/Chevereto-Chevere/src/CallableWrap.php new file mode 100644 index 000000000..c28d29ff2 --- /dev/null +++ b/Chevereto-Chevere/src/CallableWrap.php @@ -0,0 +1,287 @@ + +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. +// */ + +// namespace Chevere; + +// use LogicException; +// use ReflectionMethod; +// use ReflectionFunction; +// use ReflectionParameter; +// use ReflectionClass; +// use ReflectionFunctionAbstract; +// use Chevere\Path\Path; +// use Chevere\Path\PathHandle; + +// /** +// * Wrap provides a object oriented way to interact with Chevere controllers. +// * +// * Accepted callable strings are: +// * +// * - A callable (function, method name) +// * - A class implementing ::__invoke +// * - A fileHandle string representing the path of a file wich returns a callable +// */ +// final class CallableWrap +// { +// const TYPE_FUNCTION = 'function'; +// const TYPE_METHOD = 'method'; +// const TYPE_CLASS = 'class'; + +// const TYPES = [self::TYPE_FUNCTION, self::TYPE_CLASS]; + +// /** @var string The callable string handle used to construct the object */ +// private $callableHandle; + +// /** @var array An array containg typehinted arguments ready to use */ +// private $typedArguments; + +// /** @var ReflectionMethod The reflected callable (method) */ +// private $reflectionMethod; + +// /** @var ReflectionFunction The reflected callable (function) */ +// private $reflectionFunction; + +// /** @var ReflectionFunctionAbstract */ +// private $reflection; + +// /** @var callable The actual callable */ +// private $callable; + +// /** @var string The callable file (if any) */ +// private $callableFilepath; + +// /** @var string The callable type (function, method, class) */ +// private $type; + +// /** @var string Class name (if any) */ +// private $class; + +// /** @var string Method name (if any) */ +// private $method; + +// /** @var string[] Callable parameters */ +// // private $parameters; + +// /** @var array Callable arguments */ +// private $arguments; + +// /** @var array Passed callable arguments */ +// private $passedArguments; + +// /** @var bool True if the callable comes from a fileHandle */ +// private $isFileHandle; + +// /** @var bool True if the callable represents a anon function or class */ +// private $isAnon; + +// public function __construct(string $callableHandle) +// { +// $this->isFileHandle = false; +// $this->callableHandle = $callableHandle; +// // Direct processing for callable strings and invocable classes +// if (is_callable($this->callableHandle)) { +// $this->callable = $callableHandle; +// $this->isAnon = false; +// $this->process(); +// } else { +// if (class_exists($callableHandle)) { +// $this->handleCallableClass($callableHandle); +// } +// } +// // Some work needed when dealing with fileHandle +// if (!isset($this->callable)) { +// if (Utility\Str::contains('::', $this->callableHandle)) { +// $explode = explode('::', $this->callableHandle); +// $this->class = $explode[0]; +// $this->method = $explode[1]; +// $this->handleCallableClassMethod($this->class, $this->method); +// } else { +// $this->handleCallableFile($this->callableHandle); +// $this->process(); +// } +// } +// } + +// /** +// * Pass arguments to the callable which will be typehinted by this class. +// * +// * @param array $passedArguments +// * +// * @return self +// */ +// public function setPassedArguments(array $passedArguments): self +// { +// $this->passedArguments = $passedArguments; + +// return $this; +// } + +// public function getArguments(): array +// { +// if (!isset($this->arguments)) { +// $this->processArguments(); +// } + +// return $this->arguments ?? []; +// } + +// private function handleCallableClass(string $callableClass): void +// { +// if (method_exists($callableClass, '__invoke')) { +// $this->callable = new $callableClass(); +// $this->class = $callableClass; +// $this->method = '__invoke'; +// $this->isAnon = false; +// $this->process(); +// } else { +// throw new LogicException( +// (new Message('Missing magic method %s in class %c.')) +// ->code('%s', '__invoke') +// ->code('%c', $callableClass) +// ->toString() +// ); +// } +// } + +// private function handleCallableClassMethod(string $class, string $method): void +// { +// if (!class_exists($class)) { +// throw new LogicException( +// (new Message('Callable string handle targeting not found class %c.')) +// ->code('%c', $class) +// ->toString() +// ); +// } +// if (0 === strpos($method, '__')) { +// throw new LogicException( +// (new Message('Callable string handle targeting magic method %m.')) +// ->code('%m', $method) +// ->toString() +// ); +// } +// if (!method_exists($class, $method)) { +// throw new LogicException( +// (new Message('Callable string handle targeting an nonexistent method %m.')) +// ->code('%m', $method) +// ->toString() +// ); +// } +// } + +// private function handleCallableFile(string $callableIdentifier): void +// { +// $callableFilepath = Path::fromIdentifier($callableIdentifier); +// if (!File::exists($callableFilepath)) { +// throw new LogicException( +// (new Message('Unable to locate a callable specified by %s.')) +// ->code('%s', $callableIdentifier) +// ->toString() +// ); +// } +// $callable = include $callableFilepath; +// if (!is_callable($callable)) { +// throw new LogicException( +// (new Message('Expected %s callable, %t provided in %f.')) +// ->code('%s', '$callable') +// ->code('%t', gettype($callable)) +// ->code('%f', $callableIdentifier) +// ->toString() +// ); +// } +// $this->isFileHandle = true; +// $this->callable = $callable; +// $this->callableFilepath = $callableFilepath; +// } + +// private function validateType(string $type) +// { +// if (!in_array($type, static::TYPES)) { +// throw new LogicException( +// (new Message('Invalid type %s, expecting one of these: %v.')) +// ->code('%s', $type) +// ->code('%v', implode(', ', static::TYPES)) +// ->toString() +// ); +// } +// } + +// // Process the callable and fill the object properties +// private function process() +// { +// if (isset($this->class)) { +// $this->type = isset($this->method) ? static::TYPE_CLASS : static::TYPE_FUNCTION; +// } else { +// if (is_object($this->callable)) { +// $this->method = '__invoke'; +// $reflection = new ReflectionClass($this->callable); +// $this->type = static::TYPE_CLASS; +// $this->isAnon = $reflection->isAnonymous(); +// $this->class = $this->isAnon ? 'class@anonymous' : $reflection->getName(); +// } else { +// $this->type = static::TYPE_FUNCTION; +// } +// } +// $this->validateType($this->type); +// } + +// private function processReflection(): self +// { +// if ($this->reflectionFunction) { +// return $this; +// } +// if (is_object($this->callable)) { +// $this->reflectionMethod = new ReflectionMethod($this->callable, $this->method); +// } else { +// $this->reflectionFunction = new ReflectionFunction($this->callable); +// } +// $this->reflection = $this->reflectionFunction ?? $this->reflectionMethod; + +// return $this; +// } + +// private function processArguments(): self +// { +// $this->processReflection(); +// $this->typedArguments = []; +// $parameterIndex = 0; + +// // Magically create typehinted arguments +// foreach ($this->reflection->getParameters() as $parameter) { +// $type = null; +// $parameterType = $parameter->getType(); +// if (isset($parameterType)) { +// $type = $parameterType->getName(); +// } +// $this->processTypedArgument( +// $parameter, +// $type, +// $this->passedArguments[$parameter->getName()] ?? $this->passedArguments[$parameterIndex] ?? null +// ); +// ++$parameterIndex; +// } +// $this->arguments = $this->typedArguments; + +// return $this; +// } + +// private function processTypedArgument(ReflectionParameter $parameter, string $type = null, $value = null): void +// { +// if (!isset($type) || in_array($type, Controller::TYPE_DECLARATIONS)) { +// $this->typedArguments[] = $value ?? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null); +// } elseif (null === $value && $parameter->allowsNull()) { +// $this->typedArguments[] = null; +// } else { +// $this->typedArguments[] = new $type($value); +// } +// } +// } diff --git a/Chevereto-Chevere/src/Console/Cli.php b/Chevereto-Chevere/src/Console/Cli.php new file mode 100644 index 000000000..faed38a64 --- /dev/null +++ b/Chevereto-Chevere/src/Console/Cli.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Console; + +use Monolog\Logger; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\ConsoleOutput; +use Chevere\Console\Commands\BuildCommand; +use Chevere\Console\Commands\RequestCommand; +use Chevere\Console\Commands\RunCommand; +use Chevere\Console\Commands\InspectCommand; +use Chevere\Contracts\Console\CliContract; +use Chevere\Contracts\Console\CommandContract; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * This class provides Chevere CLI. + */ +final class Cli implements CliContract +{ + const NAME = __NAMESPACE__ . ' cli'; + const VERSION = '1.0'; + + /** @var string Cli name */ + private $name; + + /** @var string Cli version */ + private $version; + + /** @var ArgvInput */ + private $input; + + /** @var ConsoleOutput */ + private $output; + + /** @var Logger */ + private $logger; + + /** @var Application */ + private $client; + + /** @var SymfonyStyle */ + private $style; + + /** @var CommandContract */ + private $command; + + public function __construct(ArgvInput $input) + { + $this->input = $input; + $this->name = static::NAME; + $this->version = static::VERSION; + $this->output = new ConsoleOutput(); + $this->client = new Application($this->name, $this->version); + $this->client->setAutoExit(false); + $this->logger = new Logger($this->name); + $this->style = new SymfonyStyle($this->input, $this->output); + + $this->client->addCommands([ + (new RequestCommand($this))->symfony(), + (new RunCommand($this))->symfony(), + (new InspectCommand($this))->symfony(), + (new BuildCommand($this))->symfony(), + ]); + + $command = Console::command(); + try { + $this->client->get($command); + } catch (CommandNotFoundException $e) { + // Shhhh, let Fabien's handle this... + } + } + + public function input(): ArgvInput + { + return $this->input; + } + + public function style(): SymfonyStyle + { + return $this->style; + } + + public function output(): ConsoleOutput + { + return $this->output; + } + + public function logger(): Logger + { + return $this->logger; + } + + public function setCommand(CommandContract $command): void + { + $this->command = $command; + } + + public function command(): CommandContract + { + return $this->command; + } + + /** + * {@inheritdoc} + */ + public function runner(): int + { + return $this->client->run($this->input, $this->output); + } +} diff --git a/Chevereto-Chevere/src/Console/Command.php b/Chevereto-Chevere/src/Console/Command.php new file mode 100644 index 000000000..74caf5c87 --- /dev/null +++ b/Chevereto-Chevere/src/Console/Command.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Console; + +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Chevere\Contracts\App\LoaderContract; +use Chevere\Contracts\Console\CliContract; +use Chevere\Contracts\Console\CommandContract; +use Chevere\Contracts\Console\SymfonyCommandContract; + +/** + * This is the base command of all Chevere commands. + */ +abstract class Command implements CommandContract +{ + const ARGUMENT_REQUIRED = InputArgument::REQUIRED; + const ARGUMENT_OPTIONAL = InputArgument::OPTIONAL; + const ARGUMENT_IS_ARRAY = InputArgument::IS_ARRAY; + + const OPTION_NONE = InputOption::VALUE_NONE; + const OPTION_REQUIRED = InputOption::VALUE_REQUIRED; + const OPTION_OPTIONAL = InputOption::VALUE_OPTIONAL; + const OPTION_IS_ARRAY = InputOption::VALUE_IS_ARRAY; + + const NAME = ''; + const DESCRIPTION = ''; + const HELP = ''; + + const ARGUMENTS = []; + const OPTIONS = []; + + /** @var CliContract */ + private $cli; + + /** @var SymfonyCommandContract */ + private $symfony; + + final public function __construct(CliContract $cli) + { + $this->cli = $cli; + $this->symfony = new SymfonyCommand($this); + $this->configure(); + } + + final public function cli(): CliContract + { + return $this->cli; + } + + final public function symfony(): SymfonyCommandContract + { + return $this->symfony; + } + + final public function getArgument(string $argument) + { + return $this->cli->input()->getArgument($argument); + } + + final public function getOption(string $option) + { + return $this->cli->input()->getOption($option); + } + + abstract public function callback(LoaderContract $loader): int; + + final protected function configure(): void + { + $this->symfony + ->setName(static::NAME) + ->setDescription(static::DESCRIPTION) + ->setHelp(static::HELP); + foreach (static::ARGUMENTS as $arguments) { + $this->symfony->addArgument(...$arguments); + } + foreach (static::OPTIONS as $options) { + $this->symfony->addOption(...$options); + } + } +} diff --git a/Chevereto-Chevere/src/Console/Commands/BuildCommand.php b/Chevereto-Chevere/src/Console/Commands/BuildCommand.php new file mode 100644 index 000000000..bd46fa28e --- /dev/null +++ b/Chevereto-Chevere/src/Console/Commands/BuildCommand.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Console\Commands; + +use Chevere\Console\Command; +use Chevere\Contracts\App\LoaderContract; + +/** + * The BuildCommand builds the App. + * + * Usage: + * php app/console build + */ +final class BuildCommand extends Command +{ + const NAME = 'build'; + const DESCRIPTION = 'Build the App'; + const HELP = 'This command builds the App'; + + public function callback(LoaderContract $loader): int + { + $loader->build(); + $this->cli()->style()->block('App built', 'SUCCESS', 'fg=black;bg=green', ' ', true); + $checksums = []; + foreach ($loader->cacheChecksums() as $name => $keys) { + foreach ($keys as $key => $array) { + $checksums[] = [$name, $key, $array['path'], substr($array['checksum'], 0, 8)]; + } + } + $this->cli()->style()->table(['Cache', 'Key', 'Path', 'Checksum'], $checksums); + return 0; + } +} diff --git a/Chevereto-Chevere/src/Console/Commands/InspectCommand.php b/Chevereto-Chevere/src/Console/Commands/InspectCommand.php new file mode 100644 index 000000000..ea08076d7 --- /dev/null +++ b/Chevereto-Chevere/src/Console/Commands/InspectCommand.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// TODO: Static inspection! +// TODO: Deprecate callables by file +// TODO: Non-ambiguous types. Use $callableString $callableObject? + +namespace Chevere\Console\Commands; + +use Reflector; +use ReflectionMethod; +use ReflectionFunction; +use Chevere\Contracts\App\LoaderContract; +use Chevere\Contracts\Controller\ControllerContract; +use Chevere\Console\Command; +use Chevere\Utility\Str; + +/** + * The InspectCommand allows to get callable information using CLI. + * + * Usage: + * php app/console inspect "callable" + */ +final class InspectCommand extends Command +{ + const NAME = 'inspect'; + const DESCRIPTION = 'Inspect any callable'; + const HELP = 'This command allows you to inspect any callable'; + + const ARGUMENTS = [ + ['callable', Command::ARGUMENT_REQUIRED, 'A fully-qualified callable name'], + ]; + + /** @var array */ + protected $arguments = []; + + /** @var Reflector */ + protected $reflector; + + /** @var object|string */ + protected $callable; + + /** @var string */ + protected $method; + + /** @var string */ + protected $callableInput; + + public function callback(LoaderContract $loader): int + { + $this->callableInput = (string) $this->cli()->input()->getArgument('callable'); + if (is_subclass_of($this->callableInput, ControllerContract::class)) { + $this->callable = $this->callableInput; + $this->method = '__invoke'; + } else { + if (is_callable($this->callableInput)) { + $this->callable = $this->callableInput; + } + } + + $this->handleSetMethod(); + $this->handleSetReflector(); + $this->cli()->style()->block($this->callableInput, 'INSPECTED', 'fg=black;bg=green', ' ', true); + $this->processParametersArguments(); + $this->handleProcessArguments(); + + return 1; + } + + private function handleSetMethod(): void + { + if (is_object($this->callable)) { + $this->method = '__invoke'; + } else { + if (Str::contains('::', $this->callable)) { + $callableExplode = explode('::', $this->callable); + $this->callable = $callableExplode[0]; + $this->method = $callableExplode[1]; + } + } + } + + private function handleSetReflector(): void + { + if (isset($this->method)) { + $this->setReflectionMethod(); + } else { + $this->setReflectionFunction(); + } + } + + private function setReflectionMethod() + { + $this->reflector = new ReflectionMethod($this->callable, $this->method); + } + + private function setReflectionFunction() + { + $this->reflector = new ReflectionFunction($this->callable); + } + + private function processParametersArguments(): void + { + $i = 0; + foreach ($this->reflector->getParameters() as $parameter) { + $aux = ''; + if ($parameter->getType()) { + $aux .= $parameter->getType() . ' '; + } + $aux .= '$' . $parameter->getName(); + if ($parameter->isDefaultValueAvailable()) { + $aux .= ' = ' . ($parameter->getDefaultValue() ?? 'null'); + } + // $res = $resource[$parameter->getName()] ?? null; + // if (isset($res)) { + // $aux .= ' '.VarDump::wrap(VarDump::_OPERATOR, '--description '.$res['description'].' --regex '.$res['regex']); + // } + $this->arguments[] = "#$i $aux"; + ++$i; + } + } + + private function handleProcessArguments(): void + { + if (null != $this->arguments) { + $this->processArguments(); + } else { + $this->processNoArguments(); + } + } + + private function processArguments(): void + { + $this->cli()->style()->text(['Arguments:']); + $this->cli()->style()->listing($this->arguments); + } + + private function processNoArguments(): void + { + $this->cli()->style()->text(['No arguments', null]); + } +} diff --git a/Chevereto-Chevere/src/Console/Commands/RequestCommand.php b/Chevereto-Chevere/src/Console/Commands/RequestCommand.php new file mode 100644 index 000000000..f70108aab --- /dev/null +++ b/Chevereto-Chevere/src/Console/Commands/RequestCommand.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Console\Commands; + +use InvalidArgumentException; +use JsonException; +use ReflectionMethod; +use Chevere\Http\Request; +use Chevere\Console\Command; +use Chevere\Contracts\App\LoaderContract; +use Chevere\Message; +use Chevere\Http\Response; +use Chevere\Router\Exception\RouteNotFoundException; + +/** + * The RequestCommand allows to pass a forged request to the App instance. + * + * Usage: + * php app/console request + * + * Both static::COMMANDS and static::OPTIONS are intended to match Request::create parameters names. + */ +final class RequestCommand extends Command +{ + const NAME = 'request'; + const DESCRIPTION = 'Forge and resolve a HTTP request'; + const HELP = 'This command allows you to forge a HTTP request'; + + const ARGUMENTS = [ + ['method', Command::ARGUMENT_OPTIONAL, 'HTTP request method', 'GET'], + ['uri', Command::ARGUMENT_OPTIONAL, 'URI', '/'], + ]; + + const OPTIONS = [ + [ + 'parameters', + 'p', + Command::OPTION_OPTIONAL, + 'Parameters [json]', + [] + ], + [ + 'cookies', + 'c', + Command::OPTION_OPTIONAL, + '$_COOKIE [json]', + [] + ], + [ + 'files', + 'f', + Command::OPTION_OPTIONAL, + '$_FILES [json]', + [] + ], + [ + 'server', + 's', + Command::OPTION_OPTIONAL, + '$_SERVER [json]', + [] + ], + [ + 'content', + null, + Command::OPTION_OPTIONAL, + 'Raw body data', + null + ], + [ + 'headers', + 'H', + Command::OPTION_NONE, + 'Output headers', + ], + [ + 'body', + 'B', + Command::OPTION_NONE, + 'Output body', + ], + [ + 'noformat', + 'x', + Command::OPTION_NONE, + 'No output decorations', + ], + ]; + + /** @var array */ + private $arguments; + + /** @var array */ + private $options; + + /** @var array */ + private $jsonOptions; + + // List of arguments which are passed as JSON + const JSON_OPTIONS = ['parameters', 'cookies', 'files', 'server']; + + public function callback(LoaderContract $loader): int + { + $this->arguments = $this->cli()->input()->getArguments(); + $this->options = (array) $this->cli()->input()->getOptions(); + $this->setJsonOptions(); + + $passedArguments = array_merge($this->options, $this->jsonOptions, $this->arguments); + $requestFnArguments = []; + $r = new ReflectionMethod(Request::class, 'create'); + foreach ($r->getParameters() as $requestArg) { + $name = $requestArg->getName(); + $requestFnArguments[] = $passedArguments[$name] ?? $requestArg->getDefaultValue() ?? null; + } + $loader->setRequest(Request::create(...$requestFnArguments)); + + try { + $loader->run(); + } catch (RouteNotFoundException $e) { + // $e Shhhh... This is just to capture the CLI output + } + + $response = $loader->app->response(); + $this->render($response); + + return 0; + } + + public function render(Response $response) + { + $isNoFormat = (bool) $this->getOption('noformat'); + $isHeaders = (bool) $this->getOption('headers'); + $isBody = (bool) $this->getOption('body'); + if (!$isHeaders && !$isBody) { + $isHeaders = true; + $isBody = true; + } + $status = $response->chvStatus(); + $headers = $response->chvHeaders(); + if (!$isNoFormat) { + $status = '' . $status . ''; + $headers = '' . $headers . ''; + } + $this->cli()->style()->writeln($status); + if ($isHeaders) { + $this->cli()->style()->writeln($headers); + } + if ($isBody) { + $this->cli()->style()->write($response->chvBuffer() . "\r\n"); + } + die(0); + } + + private function setJsonOptions(): void + { + $this->jsonOptions = []; + foreach (static::JSON_OPTIONS as $v) { + if (is_string($this->options[$v])) { + try { + $json = json_decode($this->options[$v], true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new InvalidArgumentException( + (new Message('Unable to parse %o option %s as JSON (%m).')) + ->code('%o', $v) + ->code('%s', $this->options[$v]) + ->strtr('%m', $e->getMessage()) + ->toString() + ); + } + } else { + $json = $this->options[$v]; + } + $this->jsonOptions[$v] = $json; + } + } +} diff --git a/Chevereto-Chevere/src/Console/Commands/RunCommand.php b/Chevereto-Chevere/src/Console/Commands/RunCommand.php new file mode 100644 index 000000000..637c807b4 --- /dev/null +++ b/Chevereto-Chevere/src/Console/Commands/RunCommand.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Console\Commands; + +use Closure; +use Chevere\Console\Command; +use Chevere\VarDump\PlainVarDump; +use Chevere\Contracts\App\LoaderContract; +use Chevere\Controller\Controller; +use Chevere\Message; +use InvalidArgumentException; +use JakubOnderka\PhpConsoleColor\ConsoleColor; +use Symfony\Component\Console\Helper\FormatterHelper; + +/** + * The RunCommand allows to run any callable present in the app. + * + * Usage: + * php app/console run + */ +final class RunCommand extends Command +{ + const NAME = 'run'; + const DESCRIPTION = 'Run any callable'; + const HELP = 'Outputs type, callable return, and buffer (if exists)'; + + const ARGUMENTS = [ + ['callable', Command::ARGUMENT_REQUIRED, 'A fully-qualified callable name or Controller'], + ]; + + const OPTIONS = [ + [ + 'argument', + 'a', + Command::OPTION_OPTIONAL | Command::OPTION_IS_ARRAY, + 'Callable arguments (in declarative order)', + ], + [ + 'return', + 'r', + Command::OPTION_NONE, + 'Output return', + ], + [ + 'buffer', + 'b', + Command::OPTION_NONE, + 'Output buffer', + ], + [ + 'noformat', + 'x', + Command::OPTION_NONE, + 'No output type nor decorations', + ], + ]; + + /** @var LoaderContract */ + private $loader; + + /** @var string */ + private $callable; + + /** @var array */ + protected $argument; + + /** @var mixed */ + private $return; + + /** @var string */ + private $export; + + /** @var string */ + private $buffer; + + /** @var bool */ + private $isNoFormat; + + /** @var bool */ + private $isReturn; + + /** @var bool */ + private $isBuffer; + + /** @var array */ + private $lines; + + public function callback(LoaderContract $loader): int + { + $this->loader = $loader; + $this->callable = (string) $this->getArgument('callable'); + $this->argument = $this->getOption('argument'); + + if (is_subclass_of($this->callable, Controller::class)) { + $this->runController(); + } else { + $this->runCallable(); + } + + return 1; + } + + private function runController(): void + { + $this->loader->setArguments($this->argument); + $this->loader->setController($this->callable); + $this->loader->run(); + } + + private function runCallable(): void + { + $this->validateCallable(); + $this->bufferedRunCallable(); + + $this->isNoFormat = (bool) $this->getOption('noformat'); + $this->isReturn = (bool) $this->getOption('return'); + $this->isBuffer = (bool) $this->getOption('buffer'); + + if (!$this->isReturn && !$this->isBuffer) { + $this->isReturn = true; + $this->isBuffer = true; + } + + $this->setLines(); + + $this->cli()->style()->writeln($this->lines); + } + + private function validateCallable(): void + { + if (class_exists($this->callable)) { + $isCallable = method_exists($this->callable, '__invoke'); + } else { + $isCallable = is_callable($this->callable); + } + if (!$isCallable) { + throw new InvalidArgumentException( + (new Message('No callable found for %s string.')) + ->code('%s', $this->callable) + ->toString() + ); + } + } + + /** + * Run the callable capturing its return and buffer. + * + * Sets @var mixed $return @var string $buffer @var string $export + */ + private function bufferedRunCallable(): void + { + ob_start(); + $callable = $this->callable; + $this->return = $callable(...$this->argument); + $buffer = ob_get_contents(); + if (false !== $buffer) { + $this->buffer = $buffer; + } + ob_end_clean(); + $this->export = var_export($this->return, true); + } + + private function setLines(): void + { + $this->lines = []; + $cc = new ConsoleColor(); + if ($this->isReturn) { + if ($this->isNoFormat) { + $this->lines = [$this->export]; + } else { + $this->lines = ['' . $cc->apply('italic', gettype($this->return)) . ' ' . $this->export]; + } + } + if ($this->isBuffer && $this->buffer != '') { + if ($this->isNoFormat) { + $this->lines[] = $this->buffer; + } else { + $this->lines[] = '' . $this->buffer . ''; + } + } + } +} diff --git a/Chevereto-Chevere/src/Console/Console.php b/Chevereto-Chevere/src/Console/Console.php new file mode 100644 index 000000000..18fbccdff --- /dev/null +++ b/Chevereto-Chevere/src/Console/Console.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Console; + +use Chevere\App\Loader; +use RuntimeException; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\ConsoleOutput; +use Chevere\Contracts\App\LoaderContract; +use Chevere\Contracts\Console\ConsoleContract; +use Chevere\Contracts\Console\CliContract; +use Chevere\Message; +use Throwable; + +/** + * Provides static access to the Chevere application console. + */ +final class Console +{ + const VERBOSITY_QUIET = ConsoleOutput::VERBOSITY_QUIET; + const VERBOSITY_NORMAL = ConsoleOutput::VERBOSITY_NORMAL; + const VERBOSITY_VERBOSE = ConsoleOutput::VERBOSITY_VERBOSE; + const VERBOSITY_VERY_VERBOSE = ConsoleOutput::VERBOSITY_VERY_VERBOSE; + const VERBOSITY_DEBUG = ConsoleOutput::VERBOSITY_DEBUG; + + const OUTPUT_NORMAL = ConsoleOutput::OUTPUT_NORMAL; + const OUTPUT_RAW = ConsoleOutput::OUTPUT_RAW; + const OUTPUT_PLAIN = ConsoleOutput::OUTPUT_PLAIN; + + /** @var LoaderContract */ + private static $loader; + + /** @var CliContract */ + private static $cli; + + /** @var bool */ + private static $available; + + /** @var string The first argument (command) passed */ + private static $command; + + public static function bind(Loader $loader): bool + { + if (php_sapi_name() == 'cli') { + self::$loader = $loader; + + return true; + } + + return false; + } + + public static function init() + { + $input = new ArgvInput(); + self::$command = $input->getFirstArgument(); + self::$cli = new Cli($input); + self::$available = true; + } + + public static function command(): string + { + return self::$command; + } + + public static function isBuilding(): bool + { + return self::isAvailable() && 'build' == self::command(); + } + + public static function cli(): CliContract + { + return self::$cli; + } + + public static function run() + { + if (!self::isAvailable()) { + throw new RuntimeException( + (new Message('Unable to call %method% when %class% is not available.')) + ->code('%method%', __METHOD__) + ->code('%class%', __CLASS__) + ->toString() + ); + } + $exitCode = self::$cli->runner(); + if (0 !== $exitCode) { + die(); + } + try { + $command = self::$cli->command(); + } catch (Throwable $e) { + exit($exitCode); + } + if (self::$loader == null) { + throw new RuntimeException('No Chevere instance is defined.'); + } + exit($command->callback(self::$loader)); + } + + public static function inputString(): string + { + if (method_exists(self::$cli->input(), '__toString')) { + return self::$cli->input()->__toString(); + } + + return ''; + } + + public static function isAvailable(): bool + { + return (bool) self::$available; + } + + public static function write(string $message, int $options = Console::OUTPUT_NORMAL): void + { + self::$cli->style()->write($message, false, $options); + } + + public static function writeln(string $message, int $options = Console::OUTPUT_NORMAL): void + { + self::$cli->style()->writeln($message, $options); + } +} diff --git a/Chevereto-Chevere/src/Console/SymfonyCommand.php b/Chevereto-Chevere/src/Console/SymfonyCommand.php new file mode 100644 index 000000000..1fad36cd5 --- /dev/null +++ b/Chevereto-Chevere/src/Console/SymfonyCommand.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Console; + +use Symfony\Component\Console\Command\Command as BaseCommand; +use Chevere\Contracts\Console\CliContract; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Chevere\Contracts\Console\CommandContract; +use Chevere\Contracts\Console\SymfonyCommandContract; + +/** + * Wrapper for Symfony\Component\Console\Command\Command + */ +final class SymfonyCommand extends BaseCommand implements SymfonyCommandContract +{ + /** @var CliContract */ + private $chevereCli; + + /** @var CommandContract */ + private $chevereCommand; + + public function __construct(CommandContract $command) + { + $this->chevereCommand = $command; + $this->chevereCli = $command->cli(); + parent::__construct(); + } + + protected function initialize(InputInterface $input, OutputInterface $output) + { + $this->chevereCli->setCommand($this->chevereCommand); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + return 0; + } +} diff --git a/Chevereto-Chevere/src/Contracts/Api/ApiContract.php b/Chevereto-Chevere/src/Contracts/Api/ApiContract.php new file mode 100644 index 000000000..02826e74c --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Api/ApiContract.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Api; + +interface ApiContract +{ + public static function endpoint(string $uriKey): array; + + public static function endpointKey(string $uri): string; +} diff --git a/Chevereto-Chevere/src/Contracts/Api/MakerContract.php b/Chevereto-Chevere/src/Contracts/Api/MakerContract.php new file mode 100644 index 000000000..af39cbfcf --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Api/MakerContract.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Api; + +use Chevere\Path\PathHandle; +use Chevere\Contracts\Router\RouterContract; +use Chevere\Http\Methods; + +interface MakerContract +{ + public function __construct(RouterContract $router); + + /** + * Automatically finds controllers in the given path and generate the API route binding. + */ + public function register(PathHandle $pathHandle, Methods $methods): void; + + public function api(): array; +} diff --git a/Chevereto-Chevere/src/Contracts/Api/src/EndpointContract.php b/Chevereto-Chevere/src/Contracts/Api/src/EndpointContract.php new file mode 100644 index 000000000..bd848fc89 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Api/src/EndpointContract.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Api\src; + +use Chevere\Contracts\Http\MethodsContract; + +interface EndpointContract +{ + public function __construct(MethodsContract $methods); + + public function methods(): MethodsContract; + + public function toArray(): array; + + public function setResource(array $resource): void; +} diff --git a/Chevereto-Chevere/src/Contracts/Api/src/FilterIteratorContract.php b/Chevereto-Chevere/src/Contracts/Api/src/FilterIteratorContract.php new file mode 100644 index 000000000..8c86b4494 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Api/src/FilterIteratorContract.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Api\src; + +interface FilterIteratorContract +{ + /** + * @param array $methods Accepted HTTP methods [GET,POST,etc.] + * @param string $methodPrefix Method prefix used for root endpoint (no resource) + * + * @return self + */ + public function generateAcceptedFilenames(array $methods, string $methodPrefix): FilterIteratorContract; + + /** + * Overrides default getChildren to support the filter. + */ + public function getChildren(); + + /** + * The filter accept function. + */ + public function accept(): bool; +} diff --git a/Chevereto-Chevere/src/Contracts/App/AppContract.php b/Chevereto-Chevere/src/Contracts/App/AppContract.php new file mode 100644 index 000000000..36ffb8e80 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/App/AppContract.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\App; + +use Chevere\Contracts\Api\ApiContract; +use Chevere\Contracts\Controller\ControllerContract; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Contracts\Router\RouterContract; +use Chevere\Http\Response; + +interface AppContract +{ + public function setApi(ApiContract $api): void; + + public function setResponse(Response $response): void; + + public function setRoute(RouteContract $route): void; + + public function setRouter(RouterContract $router): void; + + public function setArguments(array $arguments): void; + + public function api(): ApiContract; + + // public function response(): Response; + + public function route(): RouteContract; + + public function router(): RouterContract; + + public function arguments(): array; + + /** + * Run a controller on top of the App. + * + * @param string $controller a ControllerContract controller name + */ + public function run(string $controller): ControllerContract; +} diff --git a/Chevereto-Chevere/src/Contracts/App/CheckoutContract.php b/Chevereto-Chevere/src/Contracts/App/CheckoutContract.php new file mode 100644 index 000000000..acd0334e9 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/App/CheckoutContract.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\App; + +interface CheckoutContract +{ + public function __construct(string $filename); +} diff --git a/Chevereto-Chevere/src/Contracts/App/LoaderContract.php b/Chevereto-Chevere/src/Contracts/App/LoaderContract.php new file mode 100644 index 000000000..0c9bf3407 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/App/LoaderContract.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\App; + +use Chevere\Http\Request; +use Chevere\Runtime\Runtime; + +interface LoaderContract +{ + public function __construct(); + + public function build(): void; + + /** + * @param string $controller a fully-qualified controller name + */ + public function setController(string $controller): void; + + /** + * @param array $arguments string arguments to pass to the controller + */ + // TODO: $arguments Datastructure + public function setArguments(array $arguments): LoaderContract; + + public function setRequest(Request $request): void; + + public static function setDefaultRuntime(Runtime $runtime); + + /** + * Run the controller. + */ + public function run(): void; + + /** + * Retrieve the loaded Runtime. + */ + public static function runtime(): Runtime; + + /** + * Retrieve the loaded Request. + */ + public static function request(): Request; + + /** + * Retrieves the file checksums, available only when building the App. + */ + public function cacheChecksums(): array; +} diff --git a/Chevereto-Chevere/src/Contracts/App/ParametersContract.php b/Chevereto-Chevere/src/Contracts/App/ParametersContract.php new file mode 100644 index 000000000..339536920 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/App/ParametersContract.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\App; + +use Chevere\ArrayFile\ArrayFile; + +interface ParametersContract +{ + public function __construct(ArrayFile $arrayFile); +} diff --git a/Chevereto-Chevere/src/Contracts/Console/CliContract.php b/Chevereto-Chevere/src/Contracts/Console/CliContract.php new file mode 100644 index 000000000..e4332ca62 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Console/CliContract.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Console; + +use Exception; +use Monolog\Logger; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Style\SymfonyStyle; + +interface CliContract +{ + public function __construct(ArgvInput $input); + + public function input(): ArgvInput; + + public function style(): SymfonyStyle; + + public function output(): ConsoleOutput; + + public function logger(): Logger; + + public function setCommand(CommandContract $command): void; + + public function command(): CommandContract; + + /** + * Runs the current command. + * + * @return int 0 if everything went fine, or an error code + * + * @throws Exception When running fails. Bypass this when {@link setCatchExceptions()}. + */ + public function runner(): int; +} diff --git a/Chevereto-Chevere/src/Contracts/Console/CommandContract.php b/Chevereto-Chevere/src/Contracts/Console/CommandContract.php new file mode 100644 index 000000000..211c0de1c --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Console/CommandContract.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Console; + +use Chevere\Contracts\App\LoaderContract; + +interface CommandContract +{ + public function __construct(CliContract $cli); + + public function cli(): CliContract; + + public function symfony(): SymfonyCommandContract; + + public function callback(LoaderContract $loader): int; +} diff --git a/Chevereto-Chevere/src/Contracts/Console/ConsoleContract.php b/Chevereto-Chevere/src/Contracts/Console/ConsoleContract.php new file mode 100644 index 000000000..68a634a8f --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Console/ConsoleContract.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Console; + +use Chevere\Console\Console; +use Chevere\Contracts\App\LoaderContract; + +interface ConsoleContract +{ + public static function bind(LoaderContract $loader): bool; + + public static function init(); + + public static function cli(): CliContract; + + public static function run(); + + public static function inputString(): string; + + public static function isRunning(): bool; + + /** + * Write messages to the console. + * + * @param string $message the message string + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + */ + public static function write(string $message, int $options = Console::OUTPUT_NORMAL): void; + + /** + * Write messages (new lines) to the console. + * + * @param string|array $message the message string + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + */ + public static function writeln(string $message, int $options = Console::OUTPUT_NORMAL): void; + + public static function log(string $message); +} diff --git a/Chevereto-Chevere/src/Contracts/Console/SymfonyCommandContract.php b/Chevereto-Chevere/src/Contracts/Console/SymfonyCommandContract.php new file mode 100644 index 000000000..dd7768042 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Console/SymfonyCommandContract.php @@ -0,0 +1,312 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Console; + +use InvalidArgumentException; +use LogicException; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Except for __construct, all methods here are taken from Symfony\Component\Console\Command\Command + */ +interface SymfonyCommandContract +{ + public function __construct(CommandContract $command); + + /** + * @return string|null The default command name or null when no default name is set + */ + public static function getDefaultName(); + + /** + * Ignores validation errors. + * + * This is mainly useful for the help command. + */ + public function ignoreValidationErrors(); + + public function setApplication(Application $application = null); + + public function setHelperSet(HelperSet $helperSet); + + /** + * Gets the helper set. + * + * @return HelperSet A HelperSet instance + */ + public function getHelperSet(); + + /** + * Gets the application instance for this command. + * + * @return Application An Application instance + */ + public function getApplication(); + + /** + * Checks whether the command is enabled or not in the current environment. + * + * Override this to check for x or y and return false if the command can not + * run properly under the current conditions. + * + * @return bool + */ + public function isEnabled(); + + /** + * Runs the command. + * + * The code to execute is either defined directly with the + * setCode() method or by overriding the execute() method + * in a sub-class. + * + * @return int The command exit code + * + * @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}. + * + * @see setCode() + * @see execute() + */ + public function run(InputInterface $input, OutputInterface $output); + + /** + * Sets the code to execute when running this command. + * + * If this method is used, it overrides the code defined + * in the execute() method. + * + * @param callable $code A callable(InputInterface $input, OutputInterface $output) + * + * @return $this + * + * @throws InvalidArgumentException + * + * @see execute() + */ + public function setCode(callable $code); + + /** + * Merges the application definition with the command definition. + * + * This method is not part of public API and should not be used directly. + * + * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments + */ + public function mergeApplicationDefinition($mergeArgs = true); + + /** + * Sets an array of argument and option instances. + * + * @param array|InputDefinition $definition An array of argument and option instances or a definition instance + * + * @return $this + */ + public function setDefinition($definition); + + /** + * Gets the InputDefinition attached to this Command. + * + * @return InputDefinition An InputDefinition instance + */ + public function getDefinition(); + + /** + * Gets the InputDefinition to be used to create representations of this Command. + * + * Can be overridden to provide the original command representation when it would otherwise + * be changed by merging with the application InputDefinition. + * + * This method is not part of public API and should not be used directly. + * + * @return InputDefinition An InputDefinition instance + */ + public function getNativeDefinition(); + + /** + * Adds an argument. + * + * @param string $name The argument name + * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @param string $description A description text + * @param string|string[]|null $default The default value (for InputArgument::OPTIONAL mode only) + * + * @throws InvalidArgumentException When argument mode is not valid + * + * @return $this + */ + public function addArgument($name, $mode = null, $description = '', $default = null); + + /** + * Adds an option. + * + * @param string $name The option name + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants + * @param string $description A description text + * @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE) + * + * @throws InvalidArgumentException If option mode is invalid or incompatible + * + * @return $this + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null); + + /** + * Sets the name of the command. + * + * This method can set both the namespace and the name if + * you separate them by a colon (:) + * + * $command->setName('foo:bar'); + * + * @param string $name The command name + * + * @return $this + * + * @throws InvalidArgumentException When the name is invalid + */ + public function setName($name); + + /** + * Sets the process title of the command. + * + * This feature should be used only when creating a long process command, + * like a daemon. + * + * PHP 5.5+ or the proctitle PECL library is required + * + * @param string $title The process title + * + * @return $this + */ + public function setProcessTitle($title); + + /** + * Returns the command name. + * + * @return string The command name + */ + public function getName(); + + /** + * @param bool $hidden Whether or not the command should be hidden from the list of commands + * + * @return Command The current instance + */ + public function setHidden($hidden); + + /** + * @return bool whether the command should be publicly shown or not + */ + public function isHidden(); + + /** + * Sets the description for the command. + * + * @param string $description The description for the command + * + * @return $this + */ + public function setDescription($description); + + /** + * Returns the description for the command. + * + * @return string The description for the command + */ + public function getDescription(); + + /** + * Sets the help for the command. + * + * @param string $help The help for the command + * + * @return $this + */ + public function setHelp($help); + + /** + * Returns the help for the command. + * + * @return string The help for the command + */ + public function getHelp(); + + /** + * Returns the processed help for the command replacing the %command.name% and + * %command.full_name% patterns with the real values dynamically. + * + * @return string The processed help for the command + */ + public function getProcessedHelp(); + + /** + * Sets the aliases for the command. + * + * @param string[] $aliases An array of aliases for the command + * + * @return $this + * + * @throws InvalidArgumentException When an alias is invalid + */ + public function setAliases($aliases); + + /** + * Returns the aliases for the command. + * + * @return array An array of aliases for the command + */ + public function getAliases(); + + /** + * Returns the synopsis for the command. + * + * @param bool $short Whether to show the short version of the synopsis (with options folded) or not + * + * @return string The synopsis + */ + public function getSynopsis($short = false); + + /** + * Add a command usage example. + * + * @param string $usage The usage, it'll be prefixed with the command name + * + * @return $this + */ + public function addUsage($usage); + + /** + * Returns alternative usages of the command. + * + * @return array + */ + public function getUsages(); + + /** + * Gets a helper instance by name. + * + * @param string $name The helper name + * + * @return mixed The helper value + * + * @throws LogicException if no HelperSet is defined + * @throws InvalidArgumentException if the helper is not defined + */ + public function getHelper($name); +} diff --git a/Chevereto-Chevere/src/Contracts/Controller/ArgumentsWrapContract.php b/Chevereto-Chevere/src/Contracts/Controller/ArgumentsWrapContract.php new file mode 100644 index 000000000..4f0c0f516 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Controller/ArgumentsWrapContract.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Controller; + +interface ArgumentsWrapContract +{ + public function __construct(ControllerContract $controller, array $arguments); + + public function arguments(): array; +} diff --git a/Chevereto-Chevere/src/Contracts/Controller/ControllerContract.php b/Chevereto-Chevere/src/Contracts/Controller/ControllerContract.php new file mode 100644 index 000000000..d614253e7 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Controller/ControllerContract.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Controller; + +use Chevere\Http\Response; +use Chevere\Contracts\App\AppContract; + +interface ControllerContract +{ + public function __construct(AppContract $app); + + public function setResponse(Response $response): ControllerContract; + + public static function description(): string; + + public static function resources(): array; + + public static function parameters(): array; +} diff --git a/Chevereto-Chevere/src/Contracts/Controller/InspectContract.php b/Chevereto-Chevere/src/Contracts/Controller/InspectContract.php new file mode 100644 index 000000000..a95aec4f7 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Controller/InspectContract.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Controller; + +use Chevere\Contracts\ToArrayContract; + +interface InspectContract extends ToArrayContract +{ + /** + * @param string $className A class name implementing the ControllerContract + */ + public function __construct(string $className); +} diff --git a/Chevereto-Chevere/src/Contracts/DataContract.php b/Chevereto-Chevere/src/Contracts/DataContract.php new file mode 100644 index 000000000..d42013c5a --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/DataContract.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts; + +use ArrayIterator; +use Countable; +use IteratorAggregate; + +interface DataContract extends ToArrayContract, IteratorAggregate, Countable +{ + public function __construct(); + + public function getIterator(): ArrayIterator; + + public function count(): int; + + public function set(array $data): DataContract; + + public function merge(array $data): DataContract; + + public function append($var): DataContract; + + public function get(): ?array; + + public function toArray(): array; + + public function hasKey(string $key): bool; + + public function setKey(string $key, $var): DataContract; + + public function getKey(string $key); + + public function removeKey(string $key): DataContract; +} diff --git a/Chevereto-Chevere/src/Contracts/Http/MethodContract.php b/Chevereto-Chevere/src/Contracts/Http/MethodContract.php new file mode 100644 index 000000000..f0ce3d17b --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Http/MethodContract.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Http; + +interface MethodContract +{ + public function __construct(string $method, string $controller); + + public function method(): string; + + public function controller(): string; +} diff --git a/Chevereto-Chevere/src/Contracts/Http/MethodsContract.php b/Chevereto-Chevere/src/Contracts/Http/MethodsContract.php new file mode 100644 index 000000000..b9653e603 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Http/MethodsContract.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Http; + +use IteratorAggregate; +use ArrayIterator; + +interface MethodsContract extends IteratorAggregate +{ + public function add(MethodContract $method): void; + + public function has(string $method): bool; + + public function get(string $method): string; + + public function getIterator(): ArrayIterator; +} diff --git a/Chevereto-Chevere/src/Contracts/Http/ResponseContract.php b/Chevereto-Chevere/src/Contracts/Http/ResponseContract.php new file mode 100644 index 000000000..53b2ebad6 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Http/ResponseContract.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Http; + +use Chevere\JsonApi\JsonApi; + +interface ResponseContract +{ + /** + * Set response content as JSON string with JSON headers + */ + public function setJsonContent(JsonApi $jsonApi): void; + + /** + * Get the HTTP status string + * + * @return string The HTTP status string like `HTTP/1.1 200 OK` + */ + public function getStatusString(): string; + + /** + * Returns the response without body (status + headers) + */ + public function getNoBody(): string; + + /** + * Set JSON response headers + */ + public function setJsonHeaders(): void; +} diff --git a/Chevereto-Chevere/src/Contracts/Http/Symfony/RequestContract.php b/Chevereto-Chevere/src/Contracts/Http/Symfony/RequestContract.php new file mode 100644 index 000000000..aedf2da55 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Http/Symfony/RequestContract.php @@ -0,0 +1,731 @@ + + * + * This file contains part of Symfony code. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Http\Symfony; + +use LogicException; +use InvalidArgumentException; +use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; + +interface RequestContract +{ + /** + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data + */ + public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null); + + /** + * Sets the parameters for this request. + * + * This method also re-initializes all properties. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data + */ + public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null); + + /** + * Creates a new request with values from PHP's super globals. + * + * @return SymfonyRequest + */ + public static function createFromGlobals(); + + /** + * Creates a Request based on a given URI and configuration. + * + * The information contained in the URI always take precedence + * over the other information (server and parameters). + * + * @param string $uri The URI + * @param string $method The HTTP method + * @param array $parameters The query (GET) or request (POST) parameters + * @param array $cookies The request cookies ($_COOKIE) + * @param array $files The request files ($_FILES) + * @param array $server The server parameters ($_SERVER) + * @param string|resource|null $content The raw body data + * + * @return SymfonyRequest + */ + public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null); + + /** + * Sets a callable able to create a Request instance. + * + * This is mainly useful when you need to override the Request class + * to keep BC with an existing system. It should not be used for any + * other purpose. + * + * @param callable|null $callable A PHP callable + */ + public static function setFactory($callable); + + /** + * Clones a request and overrides some of its parameters. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * + * @return SymfonyRequest + */ + public function duplicate(array $query = null, array $request = null, array $attributes = null, array $cookies = null, array $files = null, array $server = null); + + /** + * Clones the current request. + * + * Note that the session is not cloned as duplicated requests + * are most of the time sub-requests of the main one. + */ + public function __clone(); + + /** + * Returns the request as a string. + * + * @return string The request + */ + public function __toString(); + + /** + * Overrides the PHP global variables according to this request instance. + * + * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE. + * $_FILES is never overridden, see rfc1867 + */ + public function overrideGlobals(); + + /** + * Sets a list of trusted proxies. + * + * You should only list the reverse proxies that you manage directly. + * + * @param array $proxies A list of trusted proxies + * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies + * + * @throws InvalidArgumentException When $trustedHeaderSet is invalid + */ + public static function setTrustedProxies(array $proxies, int $trustedHeaderSet); + + /** + * Gets the list of trusted proxies. + * + * @return array An array of trusted proxies + */ + public static function getTrustedProxies(); + + /** + * Gets the set of trusted headers from trusted proxies. + * + * @return int A bit field of Request::HEADER_* that defines which headers are trusted from your proxies + */ + public static function getTrustedHeaderSet(); + + /** + * Sets a list of trusted host patterns. + * + * You should only list the hosts you manage using regexs. + * + * @param array $hostPatterns A list of trusted host patterns + */ + public static function setTrustedHosts(array $hostPatterns); + + /** + * Gets the list of trusted host patterns. + * + * @return array An array of trusted host patterns + */ + public static function getTrustedHosts(); + + /** + * Normalizes a query string. + * + * It builds a normalized query string, where keys/value pairs are alphabetized, + * have consistent escaping and unneeded delimiters are removed. + * + * @param string $qs Query string + * + * @return string A normalized query string for the Request + */ + public static function normalizeQueryString($qs); + + /** + * Enables support for the _method request parameter to determine the intended HTTP method. + * + * Be warned that enabling this feature might lead to CSRF issues in your code. + * Check that you are using CSRF tokens when required. + * If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered + * and used to send a "PUT" or "DELETE" request via the _method request parameter. + * If these methods are not protected against CSRF, this presents a possible vulnerability. + * + * The HTTP method can only be overridden when the real HTTP method is POST. + */ + public static function enableHttpMethodParameterOverride(); + + /** + * Checks whether support for the _method request parameter is enabled. + * + * @return bool True when the _method request parameter is enabled, false otherwise + */ + public static function getHttpMethodParameterOverride(); + + /** + * Gets a "parameter" value from any bag. + * + * This method is mainly useful for libraries that want to provide some flexibility. If you don't need the + * flexibility in controllers, it is better to explicitly get request parameters from the appropriate + * public property instead (attributes, query, request). + * + * Order of precedence: PATH (routing placeholders or custom attributes), GET, BODY + * + * @param string $key The key + * @param mixed $default The default value if the parameter key does not exist + * + * @return mixed + */ + public function get($key, $default = null); + + /** + * Gets the Session. + * + * @return SessionInterface|null The session + */ + public function getSession(); + + /** + * Whether the request contains a Session which was started in one of the + * previous requests. + * + * @return bool + */ + public function hasPreviousSession(); + + /** + * Whether the request contains a Session object. + * + * This method does not give any information about the state of the session object, + * like whether the session is started or not. It is just a way to check if this Request + * is associated with a Session instance. + * + * @return bool true when the Request contains a Session object, false otherwise + */ + public function hasSession(); + + /** + * Sets the Session. + * + * @param SessionInterface $session The Session + */ + public function setSession(SessionInterface $session); + + /** + * @internal + */ + public function setSessionFactory(callable $factory); + + /** + * Returns the client IP addresses. + * + * In the returned array the most trusted IP address is first, and the + * least trusted one last. The "real" client IP address is the last one, + * but this is also the least trusted one. Trusted proxies are stripped. + * + * Use this method carefully; you should use getClientIp() instead. + * + * @return array The client IP addresses + * + * @see getClientIp() + */ + public function getClientIps(); + + /** + * Returns the client IP address. + * + * This method can read the client IP address from the "X-Forwarded-For" header + * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For" + * header value is a comma+space separated list of IP addresses, the left-most + * being the original client, and each successive proxy that passed the request + * adding the IP address where it received the request from. + * + * @return string|null The client IP address + * + * @see getClientIps() + * @see http://en.wikipedia.org/wiki/X-Forwarded-For + */ + public function getClientIp(); + + /** + * Returns current script name. + * + * @return string + */ + public function getScriptName(); + + /** + * Returns the path being requested relative to the executed script. + * + * The path info always starts with a /. + * + * Suppose this request is instantiated from /mysite on localhost: + * + * * http://localhost/mysite returns an empty string + * * http://localhost/mysite/about returns '/about' + * * http://localhost/mysite/enco%20ded returns '/enco%20ded' + * * http://localhost/mysite/about?var=1 returns '/about' + * + * @return string The raw path (i.e. not urldecoded) + */ + public function getPathInfo(); + + /** + * Returns the root path from which this request is executed. + * + * Suppose that an index.php file instantiates this request object: + * + * * http://localhost/index.php returns an empty string + * * http://localhost/index.php/page returns an empty string + * * http://localhost/web/index.php returns '/web' + * * http://localhost/we%20b/index.php returns '/we%20b' + * + * @return string The raw path (i.e. not urldecoded) + */ + public function getBasePath(); + + /** + * Returns the root URL from which this request is executed. + * + * The base URL never ends with a /. + * + * This is similar to getBasePath(), except that it also includes the + * script filename (e.g. index.php) if one exists. + * + * @return string The raw URL (i.e. not urldecoded) + */ + public function getBaseUrl(); + + /** + * Gets the request's scheme. + * + * @return string + */ + public function getScheme(); + + /** + * Returns the port on which the request is made. + * + * This method can read the client port from the "X-Forwarded-Port" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Port" header must contain the client port. + * + * @return int|string can be a string if fetched from the server bag + */ + public function getPort(); + + /** + * Returns the user. + * + * @return string|null + */ + public function getUser(); + + /** + * Returns the password. + * + * @return string|null + */ + public function getPassword(); + + /** + * Gets the user info. + * + * @return string A user name and, optionally, scheme-specific information about how to gain authorization to access the server + */ + public function getUserInfo(); + + /** + * Returns the HTTP host being requested. + * + * The port name will be appended to the host if it's non-standard. + * + * @return string + */ + public function getHttpHost(); + + /** + * Returns the requested URI (path and query string). + * + * @return string The raw URI (i.e. not URI decoded) + */ + public function getRequestUri(); + + /** + * Gets the scheme and HTTP host. + * + * If the URL was called with basic authentication, the user + * and the password are not added to the generated string. + * + * @return string The scheme and HTTP host + */ + public function getSchemeAndHttpHost(); + + /** + * Generates a normalized URI (URL) for the Request. + * + * @return string A normalized URI (URL) for the Request + * + * @see getQueryString() + */ + public function getUri(); + + /** + * Generates a normalized URI for the given path. + * + * @param string $path A path to use instead of the current one + * + * @return string The normalized URI for the path + */ + public function getUriForPath($path); + + /** + * Returns the path as relative reference from the current Request path. + * + * Only the URIs path component (no schema, host etc.) is relevant and must be given. + * Both paths must be absolute and not contain relative parts. + * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. + * Furthermore, they can be used to reduce the link size in documents. + * + * Example target paths, given a base path of "/a/b/c/d": + * - "/a/b/c/d" -> "" + * - "/a/b/c/" -> "./" + * - "/a/b/" -> "../" + * - "/a/b/c/other" -> "other" + * - "/a/x/y" -> "../../x/y" + * + * @param string $path The target path + * + * @return string The relative target path + */ + public function getRelativeUriForPath($path); + + /** + * Generates the normalized query string for the Request. + * + * It builds a normalized query string, where keys/value pairs are alphabetized + * and have consistent escaping. + * + * @return string|null A normalized query string for the Request + */ + public function getQueryString(); + + /** + * Checks whether the request is secure or not. + * + * This method can read the client protocol from the "X-Forwarded-Proto" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http". + * + * @return bool + */ + public function isSecure(); + + /** + * Returns the host name. + * + * This method can read the client host name from the "X-Forwarded-Host" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Host" header must contain the client host name. + * + * @return string + * + * @throws SuspiciousOperationException when the host name is invalid or not trusted + */ + public function getHost(); + + /** + * Sets the request method. + * + * @param string $method + */ + public function setMethod($method); + + /** + * Gets the request "intended" method. + * + * If the X-HTTP-Method-Override header is set, and if the method is a POST, + * then it is used to determine the "real" intended HTTP method. + * + * The _method request parameter can also be used to determine the HTTP method, + * but only if enableHttpMethodParameterOverride() has been called. + * + * The method is always an uppercased string. + * + * @return string The request method + * + * @see getRealMethod() + */ + public function getMethod(); + + /** + * Gets the "real" request method. + * + * @return string The request method + * + * @see getMethod() + */ + public function getRealMethod(); + + /** + * Gets the mime type associated with the format. + * + * @param string $format The format + * + * @return string|null The associated mime type (null if not found) + */ + public function getMimeType($format); + + /** + * Gets the mime types associated with the format. + * + * @param string $format The format + * + * @return array The associated mime types + */ + public static function getMimeTypes($format); + + /** + * Gets the format associated with the mime type. + * + * @param string $mimeType The associated mime type + * + * @return string|null The format (null if not found) + */ + public function getFormat($mimeType); + + /** + * Associates a format with mime types. + * + * @param string $format The format + * @param string|array $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) + */ + public function setFormat($format, $mimeTypes); + + /** + * Gets the request format. + * + * Here is the process to determine the format: + * + * * format defined by the user (with setRequestFormat()) + * * _format request attribute + * * $default + * + * @param string|null $default The default format + * + * @return string|null The request format + */ + public function getRequestFormat($default = 'html'); + + /** + * Sets the request format. + * + * @param string $format The request format + */ + public function setRequestFormat($format); + + /** + * Gets the format associated with the request. + * + * @return string|null The format (null if no content type is present) + */ + public function getContentType(); + + /** + * Sets the default locale. + * + * @param string $locale + */ + public function setDefaultLocale($locale); + + /** + * Get the default locale. + * + * @return string + */ + public function getDefaultLocale(); + + /** + * Sets the locale. + * + * @param string $locale + */ + public function setLocale($locale); + + /** + * Get the locale. + * + * @return string + */ + public function getLocale(); + + /** + * Checks if the request method is of specified type. + * + * @param string $method Uppercase request method (GET, POST etc) + * + * @return bool + */ + public function isMethod($method); + + /** + * Checks whether or not the method is safe. + * + * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 + * + * @param bool $andCacheable Adds the additional condition that the method should be cacheable. True by default. + * + * @return bool + */ + public function isMethodSafe(); + + /** + * Checks whether or not the method is idempotent. + * + * @return bool + */ + public function isMethodIdempotent(); + + /** + * Checks whether the method is cacheable or not. + * + * @see https://tools.ietf.org/html/rfc7231#section-4.2.3 + * + * @return bool True for GET and HEAD, false otherwise + */ + public function isMethodCacheable(); + + /** + * Returns the protocol version. + * + * If the application is behind a proxy, the protocol version used in the + * requests between the client and the proxy and between the proxy and the + * server might be different. This returns the former (from the "Via" header) + * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns + * the latter (from the "SERVER_PROTOCOL" server parameter). + * + * @return string + */ + public function getProtocolVersion(); + + /** + * Returns the request body content. + * + * @param bool $asResource If true, a resource will be returned + * + * @return string|resource The request body content or a resource to read the body stream + * + * @throws LogicException + */ + public function getContent($asResource = false); + + /** + * Gets the Etags. + * + * @return array The entity tags + */ + public function getETags(); + + /** + * @return bool + */ + public function isNoCache(); + + /** + * Returns the preferred language. + * + * @param array $locales An array of ordered available locales + * + * @return string|null The preferred locale + */ + public function getPreferredLanguage(array $locales = null); + + /** + * Gets a list of languages acceptable by the client browser. + * + * @return array Languages ordered in the user browser preferences + */ + public function getLanguages(); + + /** + * Gets a list of charsets acceptable by the client browser. + * + * @return array List of charsets in preferable order + */ + public function getCharsets(); + + /** + * Gets a list of encodings acceptable by the client browser. + * + * @return array List of encodings in preferable order + */ + public function getEncodings(); + + /** + * Gets a list of content types acceptable by the client browser. + * + * @return array List of content types in preferable order + */ + public function getAcceptableContentTypes(); + + /** + * Returns true if the request is a XMLHttpRequest. + * + * It works if your JavaScript library sets an X-Requested-With HTTP header. + * It is known to work with common JavaScript frameworks: + * + * @see http://en.wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript + * + * @return bool true if the request is an XMLHttpRequest, false otherwise + */ + public function isXmlHttpRequest(); + + /** + * Indicates whether this request originated from a trusted proxy. + * + * This can be useful to determine whether or not to trust the + * contents of a proxy-specific header. + * + * @return bool true if the request came from a trusted proxy, false otherwise + */ + public function isFromTrustedProxy(); +} diff --git a/Chevereto-Chevere/src/Contracts/Http/Symfony/ResponseContract.php b/Chevereto-Chevere/src/Contracts/Http/Symfony/ResponseContract.php new file mode 100644 index 000000000..3c0f69884 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Http/Symfony/ResponseContract.php @@ -0,0 +1,566 @@ + + * + * This file contains part of Symfony code. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Http\Symfony; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; + +interface ResponseContract +{ + /** + * @throws \InvalidArgumentException When the HTTP status code is not valid + */ + public function __construct($content = '', int $status = 200, array $headers = []); + + /** + * Factory method for chainability. + * + * Example: + * + * return Response::create($body, 200) + * ->setSharedMaxAge(300); + * + * @param mixed $content The response content, see setContent() + * @param int $status The response status code + * @param array $headers An array of response headers + * + * @return Response + */ + public static function create($content = '', $status = 200, $headers = []); + + /** + * Returns the Response as an HTTP string. + * + * The string representation of the Response is the same as the + * one that will be sent to the client only if the prepare() method + * has been called before. + * + * @return string The Response as an HTTP string + * + * @see prepare() + */ + public function __toString(); + + /** + * Clones the current Response instance. + */ + public function __clone(); + + /** + * Prepares the Response before it is sent to the client. + * + * This method tweaks the Response to ensure that it is + * compliant with RFC 2616. Most of the changes are based on + * the Request that is "associated" with this Response. + * + * @return $this + */ + public function prepare(Request $request); + + /** + * Sends HTTP headers. + * + * @return $this + */ + public function sendHeaders(); + + /** + * Sends content for the current web response. + * + * @return $this + */ + public function sendContent(); + + /** + * Sends HTTP headers and content. + * + * @return $this + */ + public function send(); + + /** + * Sets the response content. + * + * Valid types are strings, numbers, null, and objects that implement a __toString() method. + * + * @param mixed $content Content that can be cast to string + * + * @return $this + * + * @throws \UnexpectedValueException + */ + public function setContent($content); + + /** + * Gets the current response content. + * + * @return string Content + */ + public function getContent(); + + /** + * Sets the HTTP protocol version (1.0 or 1.1). + * + * @return $this + * + * @final + */ + public function setProtocolVersion(string $version); + + /** + * Gets the HTTP protocol version. + * + * @final + */ + public function getProtocolVersion(): string; + + /** + * Sets the response status code. + * + * If the status text is null it will be automatically populated for the known + * status codes and left empty otherwise. + * + * @return $this + * + * @throws \InvalidArgumentException When the HTTP status code is not valid + * + * @final + */ + public function setStatusCode(int $code, $text = null); + + /** + * Retrieves the status code for the current web response. + * + * @final + */ + public function getStatusCode(): int; + + /** + * Sets the response charset. + * + * @return $this + * + * @final + */ + public function setCharset(string $charset); + + /** + * Retrieves the response charset. + * + * @final + */ + public function getCharset(): ?string; + + /** + * Returns true if the response may safely be kept in a shared (surrogate) cache. + * + * Responses marked "private" with an explicit Cache-Control directive are + * considered uncacheable. + * + * Responses with neither a freshness lifetime (Expires, max-age) nor cache + * validator (Last-Modified, ETag) are considered uncacheable because there is + * no way to tell when or how to remove them from the cache. + * + * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation, + * for example "status codes that are defined as cacheable by default [...] + * can be reused by a cache with heuristic expiration unless otherwise indicated" + * (https://tools.ietf.org/html/rfc7231#section-6.1) + * + * @final + */ + public function isCacheable(): bool; + + /** + * Returns true if the response is "fresh". + * + * Fresh responses may be served from cache without any interaction with the + * origin. A response is considered fresh when it includes a Cache-Control/max-age + * indicator or Expires header and the calculated age is less than the freshness lifetime. + * + * @final + */ + public function isFresh(): bool; + + /** + * Returns true if the response includes headers that can be used to validate + * the response with the origin server using a conditional GET request. + * + * @final + */ + public function isValidateable(): bool; + + /** + * Marks the response as "private". + * + * It makes the response ineligible for serving other clients. + * + * @return $this + * + * @final + */ + public function setPrivate(); + + /** + * Marks the response as "public". + * + * It makes the response eligible for serving other clients. + * + * @return $this + * + * @final + */ + public function setPublic(); + + /** + * Marks the response as "immutable". + * + * @return $this + * + * @final + */ + public function setImmutable(bool $immutable = true); + + /** + * Returns true if the response is marked as "immutable". + * + * @final + */ + public function isImmutable(): bool; + + /** + * Returns true if the response must be revalidated by caches. + * + * This method indicates that the response must not be served stale by a + * cache in any circumstance without first revalidating with the origin. + * When present, the TTL of the response should not be overridden to be + * greater than the value provided by the origin. + * + * @final + */ + public function mustRevalidate(): bool; + + /** + * Returns the Date header as a DateTime instance. + * + * @throws \RuntimeException When the header is not parseable + * + * @final + */ + public function getDate(): ?\DateTimeInterface; + + /** + * Sets the Date header. + * + * @return $this + * + * @final + */ + public function setDate(\DateTimeInterface $date); + + /** + * Returns the age of the response in seconds. + * + * @final + */ + public function getAge(): int; + + /** + * Marks the response stale by setting the Age header to be equal to the maximum age of the response. + * + * @return $this + */ + public function expire(); + + /** + * Returns the value of the Expires header as a DateTime instance. + * + * @final + */ + public function getExpires(): ?\DateTimeInterface; + + /** + * Sets the Expires HTTP header with a DateTime instance. + * + * Passing null as value will remove the header. + * + * @return $this + * + * @final + */ + public function setExpires(\DateTimeInterface $date = null); + + /** + * Returns the number of seconds after the time specified in the response's Date + * header when the response should no longer be considered fresh. + * + * First, it checks for a s-maxage directive, then a max-age directive, and then it falls + * back on an expires header. It returns null when no maximum age can be established. + * + * @final + */ + public function getMaxAge(): ?int; + + /** + * Sets the number of seconds after which the response should no longer be considered fresh. + * + * This methods sets the Cache-Control max-age directive. + * + * @return $this + * + * @final + */ + public function setMaxAge(int $value); + + /** + * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. + * + * This methods sets the Cache-Control s-maxage directive. + * + * @return $this + * + * @final + */ + public function setSharedMaxAge(int $value); + + /** + * Returns the response's time-to-live in seconds. + * + * It returns null when no freshness information is present in the response. + * + * When the responses TTL is <= 0, the response may not be served from cache without first + * revalidating with the origin. + * + * @final + */ + public function getTtl(): ?int; + + /** + * Sets the response's time-to-live for shared caches in seconds. + * + * This method adjusts the Cache-Control/s-maxage directive. + * + * @return $this + * + * @final + */ + public function setTtl(int $seconds); + + /** + * Sets the response's time-to-live for private/client caches in seconds. + * + * This method adjusts the Cache-Control/max-age directive. + * + * @return $this + * + * @final + */ + public function setClientTtl(int $seconds); + + /** + * Returns the Last-Modified HTTP header as a DateTime instance. + * + * @throws \RuntimeException When the HTTP header is not parseable + * + * @final + */ + public function getLastModified(): ?\DateTimeInterface; + + /** + * Sets the Last-Modified HTTP header with a DateTime instance. + * + * Passing null as value will remove the header. + * + * @return $this + * + * @final + */ + public function setLastModified(\DateTimeInterface $date = null); + + /** + * Returns the literal value of the ETag HTTP header. + * + * @final + */ + public function getEtag(): ?string; + + /** + * Sets the ETag value. + * + * @param string|null $etag The ETag unique identifier or null to remove the header + * @param bool $weak Whether you want a weak ETag or not + * + * @return $this + * + * @final + */ + public function setEtag(string $etag = null, bool $weak = false); + + /** + * Sets the response's cache headers (validation and/or expiration). + * + * Available options are: etag, last_modified, max_age, s_maxage, private, public and immutable. + * + * @return $this + * + * @throws \InvalidArgumentException + * + * @final + */ + public function setCache(array $options); + + /** + * Modifies the response so that it conforms to the rules defined for a 304 status code. + * + * This sets the status, removes the body, and discards any headers + * that MUST NOT be included in 304 responses. + * + * @return $this + * + * @see http://tools.ietf.org/html/rfc2616#section-10.3.5 + * + * @final + */ + public function setNotModified(); + + /** + * Returns true if the response includes a Vary header. + * + * @final + */ + public function hasVary(): bool; + + /** + * Returns an array of header names given in the Vary header. + * + * @final + */ + public function getVary(): array; + + /** + * Sets the Vary header. + * + * @param string|array $headers + * @param bool $replace Whether to replace the actual value or not (true by default) + * + * @return $this + * + * @final + */ + public function setVary($headers, bool $replace = true); + + /** + * Determines if the Response validators (ETag, Last-Modified) match + * a conditional value specified in the Request. + * + * If the Response is not modified, it sets the status code to 304 and + * removes the actual content by calling the setNotModified() method. + * + * @return bool true if the Response validators match the Request, false otherwise + * + * @final + */ + public function isNotModified(Request $request): bool; + + /** + * Is response invalid? + * + * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + * + * @final + */ + public function isInvalid(): bool; + + /** + * Is response informative? + * + * @final + */ + public function isInformational(): bool; + + /** + * Is response successful? + * + * @final + */ + public function isSuccessful(): bool; + + /** + * Is the response a redirect? + * + * @final + */ + public function isRedirection(): bool; + + /** + * Is there a client error? + * + * @final + */ + public function isClientError(): bool; + + /** + * Was there a server side error? + * + * @final + */ + public function isServerError(): bool; + + /** + * Is the response OK? + * + * @final + */ + public function isOk(): bool; + + /** + * Is the response forbidden? + * + * @final + */ + public function isForbidden(): bool; + + /** + * Is the response a not found error? + * + * @final + */ + public function isNotFound(): bool; + + /** + * Is the response a redirect of some form? + * + * @final + */ + public function isRedirect(string $location = null): bool; + + /** + * Is the response empty? + * + * @final + */ + public function isEmpty(): bool; + + /** + * Cleans or flushes output buffers up to target level. + * + * Resulting level can be greater than target level if a non-removable buffer has been encountered. + * + * @final + */ + public static function closeOutputBuffers(int $targetLevel, bool $flush); +} diff --git a/Chevereto-Chevere/src/Contracts/Render/RenderContract.php b/Chevereto-Chevere/src/Contracts/Render/RenderContract.php new file mode 100644 index 000000000..17a2ef10f --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Render/RenderContract.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Render; + +interface RenderContract +{ + public function render(); +} diff --git a/Chevereto-Chevere/src/Contracts/Route/PathValidateContract.php b/Chevereto-Chevere/src/Contracts/Route/PathValidateContract.php new file mode 100644 index 000000000..4c23d4932 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Route/PathValidateContract.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Route; + +interface PathValidateContract +{ + public function __construct(string $path); + + public function path(): string; + + public function hasHandlebars(): bool; +} diff --git a/Chevereto-Chevere/src/Contracts/Route/RouteContract.php b/Chevereto-Chevere/src/Contracts/Route/RouteContract.php new file mode 100644 index 000000000..5d9d19c71 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Route/RouteContract.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Route; + +use Chevere\Contracts\Http\MethodContract; +use Chevere\Contracts\Http\MethodsContract; + +interface RouteContract +{ + /** + * Route constructor. + * + * @param string $uri Route uri (key string) + * @param string $controller Callable for GET + */ + public function __construct(string $uri, string $controller = null); + + public function id(): string; + + public function path(): string; + + public function name(): string; + + public function hasName(): bool; + + public function wheres(): array; + + public function middlewares(): array; + + public function wildcardName(int $key): string; + + public function keyPowerSet(): array; + + public function type(): string; + + public function regex(): string; + + /** + * @param string $name route name, must be unique + */ + public function setName(string $name): RouteContract; + + /** + * Sets where conditionals for the route wildcards. + * + * @param string $wildcardName wildcard name + * @param string $regex regex pattern + */ + public function setWhere(string $wildcardName, string $regex): RouteContract; + + /** + * Sets HTTP method to callable binding. Allocates Routes. + * + * @param MethodContract $method a HTTP method contract + */ + public function setMethod(MethodContract $method): RouteContract; + + /** + * Sets HTTP method to callable binding (multiple version). + * + * @param MethodsContract $methods a HTTP methods contract + */ + public function setMethods(MethodsContract $methods): RouteContract; + + public function setId(string $id): RouteContract; + + public function addMiddleware(string $callable): RouteContract; + + /** + * @param string $httpMethod an HTTP method + */ + public function getController(string $httpMethod): string; + + /** + * Fill object missing properties and whatnot. + */ + public function fill(): RouteContract; + + /** + * Gets route regex. + * + * @param string $pattern route path pattern (set) + */ + public function getRegex(string $pattern): string; +} diff --git a/Chevereto-Chevere/src/Contracts/Route/WildcardsContract.php b/Chevereto-Chevere/src/Contracts/Route/WildcardsContract.php new file mode 100644 index 000000000..d27173b5e --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Route/WildcardsContract.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Route; + +interface WildcardsContract +{ + public function __construct(string $path); + + public function set(): string; + + public function matches(): array; + + public function toArray(): array; + + public function keyPowerSet(): array; +} diff --git a/Chevereto-Chevere/src/Contracts/Router/ResolverContract.php b/Chevereto-Chevere/src/Contracts/Router/ResolverContract.php new file mode 100644 index 000000000..9eec4e291 --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Router/ResolverContract.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Router; + +use Chevere\Contracts\Route\RouteContract; + +interface ResolverContract +{ + public function __construct($routeSome); + + public function get(): RouteContract; +} diff --git a/Chevereto-Chevere/src/Contracts/Router/RouterContract.php b/Chevereto-Chevere/src/Contracts/Router/RouterContract.php new file mode 100644 index 000000000..fda7489de --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Router/RouterContract.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Router; + +use Chevere\Router\Maker; +use Chevere\Contracts\Route\RouteContract; + +interface RouterContract +{ + public function __construct(Maker $maker = null); + + public function arguments(): array; + + /** + * Resolve routing for the given path info, sets matched arguments. + * + * @param string $pathInfo request path + */ + public function resolve(string $pathInfo): RouteContract; +} diff --git a/Chevereto-Chevere/src/Contracts/Runtime/RuntimeSetContract.php b/Chevereto-Chevere/src/Contracts/Runtime/RuntimeSetContract.php new file mode 100644 index 000000000..1cc476b6b --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/Runtime/RuntimeSetContract.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts\Runtime; + +interface RuntimeSetContract +{ + public function __construct(string $value = null); + + public function set(): void; + + public function value(): ?string; + + public function name(): string; +} diff --git a/Chevereto-Chevere/src/Contracts/ToArrayContract.php b/Chevereto-Chevere/src/Contracts/ToArrayContract.php new file mode 100644 index 000000000..3444aec6e --- /dev/null +++ b/Chevereto-Chevere/src/Contracts/ToArrayContract.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Contracts; + +interface ToArrayContract +{ + /** + * Returns an array representing the object exposed data as array. + */ + public function toArray(): array; +} diff --git a/Chevereto-Chevere/src/Controller/ArgumentsWrap.php b/Chevereto-Chevere/src/Controller/ArgumentsWrap.php new file mode 100644 index 000000000..20191e286 --- /dev/null +++ b/Chevereto-Chevere/src/Controller/ArgumentsWrap.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controller; + +use InvalidArgumentException; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionFunctionAbstract; +use Chevere\Message; +use Chevere\Contracts\Controller\ControllerContract; + +/** + * ArgumentsWrap provides a object oriented way to retrieve typehinted arguments for the controller. + */ +final class ArgumentsWrap +{ + /** @var ControllerContract */ + private $controller; + + /** @var array Passed callable arguments */ + private $arguments; + + /** @var ReflectionFunctionAbstract */ + private $reflection; + + /** @var array Typehinted arguments ready to use */ + private $typedArguments; + + public function __construct(ControllerContract $controller, array $arguments) + { + $this->controller = $controller; + $this->arguments = $arguments; + $this->processArguments(); + } + + public function typedArguments(): array + { + return $this->typedArguments; + } + + private function processArguments() + { + $this->reflection = new ReflectionMethod($this->controller, '__invoke'); + $this->typedArguments = []; + $parameterIndex = 0; + // Magically create typehinted arguments + foreach ($this->reflection->getParameters() as $parameter) { + $name = $parameter->getName(); + if (!isset($this->arguments[$name])) { + throw new InvalidArgumentException( + (new Message('Unmatched argument %argument% in %controller%')) + ->code('%argument%', $name) + ->code('%controller%', get_class($this->controller).'::__invoke') + ->toString() + ); + } + $type = null; + $parameterType = $parameter->getType(); + if (isset($parameterType)) { + $type = $parameterType->getName(); + } + $this->processTypedArgument( + $parameter, + $type, + $this->arguments[$name] ?? $this->arguments[$parameterIndex] ?? null + ); + ++$parameterIndex; + } + } + + private function processTypedArgument(ReflectionParameter $parameter, string $type = null, $value = null): void + { + if (!isset($type) || in_array($type, Controller::TYPE_DECLARATIONS)) { + $this->typedArguments[] = $value ?? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null); + } elseif (null === $value && $parameter->allowsNull()) { + $this->typedArguments[] = null; + } else { + $this->typedArguments[] = new $type($value); + } + } +} diff --git a/Chevereto-Chevere/src/Controller/Controller.php b/Chevereto-Chevere/src/Controller/Controller.php new file mode 100644 index 000000000..7a8193653 --- /dev/null +++ b/Chevereto-Chevere/src/Controller/Controller.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controller; + +use Chevere\Http\Response; +use Chevere\Traits\HookableTrait; +use Chevere\Contracts\App\AppContract; +use Chevere\Contracts\Controller\ControllerContract; +use Chevere\Contracts\DataContract; +use Chevere\Data\Data; +use Chevere\JsonApi\JsonApi; + +// Define a hookable code entry: +// $this->hook('myHook', function ($that) use ($var) { +// $that->bar = 'foo'; // $that is $this (the controller instance) +// $var = 'foobar'; // Alters $var since it hass been passed by the 'use' constructor. +// }); + +// Hooks for 'myHook' should be defined using: +// Hook::bind('myHook@controller:file', Hook::BEFORE, function ($that) { +// $that->source .= ' nosehaceeso no'; +// }); + +/** + * Controller is the defacto controller in Chevere. + */ +class Controller implements ControllerContract +{ + use HookableTrait; + + const TYPE_DECLARATIONS = ['array', 'callable', 'bool', 'float', 'int', 'string', 'iterable']; + const OPTIONS = []; + + /** @var AppContract */ + protected $app; + + /** @var JsonApi */ + protected $document; + + /** @var string Controller description */ + protected static $description; + + /** @var array Controller resources [propName => className] */ + protected static $resources; + + /** @var array Parameters passed via headers */ + protected static $parameters; + + /** + * You must provide your own __invoke. + */ + // public function __invoke() + // { + // throw new LogicException( + // (new Message('Class %c must implement a %m method.')) + // ->code('%c', get_class($this)) + // ->code('%m', 'public __invoke') + // ->toString() + // ); + // } + + final public function __construct(AppContract $app) + { + $this->app = $app; + $this->document = new JsonApi(); + } + + final public function document(): JsonApi + { + return $this->document; + } + + final public function setResponse(Response $response): ControllerContract + { + $this->app->setResponse($response); + + return $this; + } + + final public static function description(): string + { + return static::$description ?? ''; + } + + final public static function resources(): array + { + return static::$resources ?? []; + } + + final public static function parameters(): array + { + return static::$parameters ?? []; + } +} diff --git a/Chevereto-Chevere/src/Controller/Inspect.php b/Chevereto-Chevere/src/Controller/Inspect.php new file mode 100644 index 000000000..f9bc57539 --- /dev/null +++ b/Chevereto-Chevere/src/Controller/Inspect.php @@ -0,0 +1,272 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controller; + +use LogicException; +use ReflectionClass; +use Roave\BetterReflection\BetterReflection; +use Chevere\Message; +use Chevere\Api\Api; +use Chevere\Utility\Str; +use Chevere\Contracts\Controller\ControllerContract; +use Chevere\Contracts\Controller\InspectContract; +use Chevere\Interfaces\ControllerResourceInterface; +use Chevere\Interfaces\CreateFromString; +use Chevere\Interfaces\ControllerRelationshipInterface; + +/** + * Provides information about any Controller implementing ControllerContract interface. + */ +final class Inspect implements InspectContract +{ + const METHOD_ROOT_PREFIX = Api::METHOD_ROOT_PREFIX; + + /** @var string The Controller interface */ + const INTERFACE_CONTROLLER = ControllerContract::class; + + /** @var string The Controller interface */ + const INTERFACE_CONTROLLER_RESOURCE = ControllerResourceInterface::class; + + /** @var string The CreateFromString interface */ + const INTERFACE_CREATE_FROM_STRING = CreateFromString::class; + + /** @var string The description property name */ + const PROP_DESCRIPTION = 'description'; + + /** @var string The resource property name */ + const PROP_RESOURCES = 'resources'; + + /** @var string The class name, passed in the constructor */ + public $className; + + /** @var string The class shortname name */ + public $classShortName; + + /** @var string Absolute path to the inspected class */ + public $filepath; + + /** @var string|null The HTTP METHOD tied to the passed $className */ + public $httpMethod; + + /** @var ReflectionClass The reflected controller class */ + public $reflection; + + /** @var string Controller description */ + public $description; + + /** @var array Controller parameters */ + public $parameters; + + /** @var array Controller resources */ + public $resources; + + /** @var string Controller related resource (if any) */ + public $relatedResource; + + /** @var string Controller relationship class (if any) */ + public $relationship; + + /** @var bool True if the controller class must implement RESOURCES. Prefixed Classes (_ClassName) won't be resourced. */ + public $useResource; + + /** @var array Instructions for creating resources from string [propname => [regex, description],] */ + public $resourcesFromString; + + /** @var string The path component associated with the inspected Controller, used by Api */ + public $pathComponent; + + /** @var string Same as $pathComponent but for the related relationship URL (if any) */ + public $relationshipPathComponent; + + /** @var bool True if the inspected Controller implements ControllerResourceIterface */ + public $isResource; + + /** @var bool True if the inspected Controller implements ControllerRelationshipIterface */ + public $isRelatedResource; + + /** + * {@inheritdoc} + */ + public function __construct(string $className) + { + $this->reflection = new ReflectionClass($className); + $this->className = $this->reflection->getName(); + $this->classShortName = $this->reflection->getShortName(); + $this->filepath = $this->reflection->getFileName(); + $this->isResource = $this->reflection->implementsInterface(ControllerResourceInterface::class); + $this->isRelatedResource = $this->reflection->implementsInterface(ControllerRelationshipInterface::class); + $this->useResource = $this->isResource || $this->isRelatedResource; + $this->httpMethod = Str::replaceFirst(static::METHOD_ROOT_PREFIX, '', $this->classShortName); + $this->description = $className::description(); + $this->handleResources($className); + $this->parameters = $className::parameters(); + try { + $this->handleControllerResourceInterface(); + $this->handleControllerInterface(); + $this->handleConstResourceNeed(); + $this->handleConstResourceMissed(); + $this->handleConstResourceValid(); + } catch (LogicException $e) { + throw new LogicException( + (new Message($e->getMessage())) + ->code('%interfaceController%', static::INTERFACE_CONTROLLER) + ->code('%reflectionName%', $this->reflection->getName()) + ->code('%interfaceControllerResource%', static::INTERFACE_CONTROLLER_RESOURCE) + ->code('%reflectionFilename%', $this->reflection->getFileName()) + ->code('%endpoint%', $this->httpMethod.' api/users') + ->code('%className%', $this->className) + ->code('%propResources%', 'const '.static::PROP_RESOURCES) + ->code('%filepath%', $this->filepath) + ->toString() + ); + } + $this->handleProcessResources(); + $this->processPathComponent(); + } + + public function toArray(): array + { + return [ + 'className' => $this->className, + 'httpMethod' => $this->httpMethod, + 'description' => $this->description, + 'resources' => $this->resources, + 'useResource' => $this->useResource, + 'resourcesFromString' => $this->resourcesFromString, + 'pathComponent' => $this->pathComponent, + ]; + } + + private function handleResources(string $className) + { + if ($this->isResource) { + $this->resources = $className::resources(); + } elseif ($this->isRelatedResource) { + $this->relatedResource = $className::getRelatedResource(); + if (empty($this->relatedResource)) { + throw new LogicException( + (new Message('Class %s implements %i interface, but it doesnt define any related resource.')) + ->code('%s', $className) + ->code('%i', ControllerRelationshipInterface::class) + ->toString() + ); + } + $this->resources = $this->relatedResource::resources(); + } + } + + private function handleControllerInterface(): void + { + if (!$this->reflection->implementsInterface(static::INTERFACE_CONTROLLER)) { + throw new LogicException('Class %reflectionName% must implement the %interfaceController% interface at %reflectionFilename%.'); + } + } + + private function handleControllerResourceInterface(): void + { + if (!Str::startsWith(static::METHOD_ROOT_PREFIX, $this->classShortName) && $this->useResource && !$this->reflection->implementsInterface(static::INTERFACE_CONTROLLER_RESOURCE)) { + throw new LogicException('Class %reflectionName% must implement the %interfaceControllerResource% interface at %reflectionFilename%.'); + } + } + + private function handleConstResourceNeed(): void + { + if (!empty($this->resources) && !$this->useResource) { + throw new LogicException('Class %className% defines %propResources% but this Controller class targets a non-resourced endpoint: %endpoint%. Remove the unused %propResources% declaration at %filepath%.'); + } + } + + private function handleConstResourceMissed(): void + { + if (null == $this->resources && $this->isResource) { + throw new LogicException('Class %className% must define %propResources% at %filepath%.'); + } + } + + private function handleConstResourceValid(): void + { + if (is_iterable($this->resources)) { + foreach ($this->resources as $propertyName => $className) { + if (!class_exists($className)) { + throw new LogicException( + (new Message('Class %s not found for %c Controller at %f.')) + ->code('%s', $className) + ->toString() + ); + } + } + } + } + + private function handleProcessResources(): void + { + if (is_iterable($this->resources)) { + $resourcesFromString = []; + foreach ($this->resources as $propName => $resourceClassName) { + // Better reflection is needed due to this: https://bugs.php.net/bug.php?id=69804 + $resourceReflection = (new BetterReflection()) + ->classReflector() + ->reflect($resourceClassName); + if ($resourceReflection->implementsInterface(static::INTERFACE_CREATE_FROM_STRING)) { + $resourcesFromString[$propName] = [ + 'regex' => $resourceReflection->getStaticPropertyValue('stringRegex'), + 'description' => $resourceReflection->getStaticPropertyValue('stringDescription'), + ]; + } + } + if (!empty($resourcesFromString)) { + $this->resourcesFromString = $resourcesFromString; + } + } + } + + private function processPathComponent(): void + { + $pathComponent = $this->getPathComponent($this->className); + $pathComponents = explode('/', $pathComponent); + if ($this->useResource) { + $resourceWildcard = '{'.array_keys($this->resources)[0].'}'; + if ($this->isResource) { + // Append the resource wildcard: api/users/{wildcard} + $pathComponent .= '/'.$resourceWildcard; + } elseif ($this->isRelatedResource) { + $related = array_pop($pathComponents); + // Inject the resource wildcard: api/users/{wildcard}/related + $pathComponent = implode('/', $pathComponents).'/'.$resourceWildcard.'/'.$related; + /* + * Code below generates api/users/{user}/relationships/friends (relationship URL) + * from api/users/{user}/friends (related resource URL). + */ + $pathComponentArray = explode('/', $pathComponent); + $relationship = array_pop($pathComponentArray); + $relatedPathComponentArray = array_merge($pathComponentArray, ['relationships'], [$relationship]); + // Something like api/users/{user}/relationships/friends + $relatedPathComponent = implode('/', $relatedPathComponentArray); + $this->relationshipPathComponent = $relatedPathComponent; + // ->implementsInterface(ControllerRelationshipInterface::class) + $this->relationship = $this->reflection->getParentClass()->getName(); + } + } + $this->pathComponent = $pathComponent; + } + + private function getPathComponent(string $className): string + { + $classShortName = substr($className, strrpos($className, '\\') + 1); + $classNamespace = Str::replaceLast('\\'.$classShortName, '', $className); + $classNamespaceNoApp = Str::replaceFirst('App\\', '', $classNamespace); + + return strtolower(Str::forwardSlashes($classNamespaceNoApp)); + } +} diff --git a/Chevereto-Chevere/src/Controller/Relationship.php b/Chevereto-Chevere/src/Controller/Relationship.php new file mode 100644 index 000000000..d55f5b009 --- /dev/null +++ b/Chevereto-Chevere/src/Controller/Relationship.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controller; + +use Chevere\Interfaces\ControllerRelationshipInterface; + +/** + * Abstract class used for API resourced Controllers with relationships. + */ +abstract class Relationship extends Controller implements ControllerRelationshipInterface +{ + protected static $description = 'Describes endpoint relationship.'; + + protected static $relatedResource = null; + + public static function getRelatedResource(): ?string + { + return static::$relatedResource ?? null; + } +} diff --git a/Chevereto-Chevere/src/Controller/Resource.php b/Chevereto-Chevere/src/Controller/Resource.php new file mode 100644 index 000000000..328b7b35b --- /dev/null +++ b/Chevereto-Chevere/src/Controller/Resource.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controller; + +use Chevere\Interfaces\ControllerResourceInterface; + +/** + * Abstract class used for API resourced Controllers. + */ +abstract class Resource extends Controller implements ControllerResourceInterface +{ + // protected static $description = 'Describes the endpoint resource.'; + + protected static $resources = []; + + public static function getResourceName(): string + { + return array_keys(static::resources())[0]; + } +} diff --git a/Chevereto-Chevere/src/Controllers/Api/GetController.php b/Chevereto-Chevere/src/Controllers/Api/GetController.php new file mode 100644 index 000000000..74a082781 --- /dev/null +++ b/Chevereto-Chevere/src/Controllers/Api/GetController.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controllers\Api; + +use const Chevere\CLI; +use Chevere\Console\Console; +use Chevere\Message; +use Chevere\Api\Api; +use Chevere\Controller\Controller; +use InvalidArgumentException; + +// use Chevere\JsonApi\Data; + +// TODO: Use json:api immutable + +/** + * Exposes an API endpoint. + */ +final class GetController extends Controller +{ + protected static $description = 'Retrieve endpoint.'; + + /** @var string */ + private $endpoint; + + /** + * @param string $endpoint an API endpoint (/api) + */ + public function __invoke(?string $endpoint = null) + { + if (isset($endpoint)) { + $route = $this->app->router()->resolve($endpoint); + } else { + $route = $this->app->route(); + if (isset($route)) { + $endpoint = $route->path(); + } else { + $msg = 'Must provide the %s argument when running this callable without route context.'; + $message = (new Message($msg))->code('%s', '$endpoint')->toString(); + if (CLI) { + Console::cli()->style()->error($message); + + return; + } + throw new InvalidArgumentException($message); + } + } + + if (!isset($route)) { + $response = $this->app->response(); + $response->setStatusCode(404); + + return; + } + + $this->endpoint = $endpoint; + + $this->process(); + } + + private function process() + { + $endpointData = Api::endpoint($this->endpoint); + dd($this->endpoint, $endpointData); + if ($endpointData) { + $response = $this->app->response(); + $response->setMeta(['api' => $this->endpoint]); + // foreach ($endpointData as $property => $data) { + // if ($property == 'wildcards') { + // continue; + // } + // $data = new Data($property, 'endpoint'); + // $data->addAttribute('entry', $data); + // $response->addData($data); + // } + } + $response->setStatusCode(200); + } +} diff --git a/Chevereto-Chevere/src/Controllers/Api/HeadController.php b/Chevereto-Chevere/src/Controllers/Api/HeadController.php new file mode 100644 index 000000000..e0ab6fa6b --- /dev/null +++ b/Chevereto-Chevere/src/Controllers/Api/HeadController.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controllers\Api; + +use const Chevere\CLI; +use Chevere\Console\Console; +use Chevere\Message; +use Chevere\Controller\Controller; +use Chevere\Route\Route; +use InvalidArgumentException; + +/** + * Identical to GET, but without any message-boby in the response. + */ +final class HeadController extends Controller +{ + protected static $description = 'GET without message-body.'; + + /** @var Route */ + private $route; + + public function __invoke(?string $endpoint = null) + { + if (isset($endpoint)) { + $route = $this->app->router()->resolve($endpoint); + } else { + $route = $this->app->route(); + if (!isset($route)) { + $msg = 'Must provide the %s argument when running this callable without route context.'; + $message = (new Message($msg))->code('%s', '$endpoint')->toString(); + if (CLI) { + Console::cli()->style()->error($message); + + return; + } else { + throw new InvalidArgumentException($message); + } + } + } + + if (!isset($route)) { + $this->app->response()->setStatusCode(404); + + return; + } + + $this->route = $route; + + $this->process(); + } + + private function process() + { + $controller = $this->route->getController('GET'); + $this->app->run($controller); + $this->app->response()->setContent(null); + if (CLI) { + Console::cli()->style()->block($this->app->response()->getStatusString(), 'STATUS', 'fg=black;bg=green', ' ', true); + } + } +} diff --git a/Chevereto-Chevere/src/Controllers/Api/OptionsController.php b/Chevereto-Chevere/src/Controllers/Api/OptionsController.php new file mode 100644 index 000000000..5d9dbd141 --- /dev/null +++ b/Chevereto-Chevere/src/Controllers/Api/OptionsController.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controllers\Api; + +use const Chevere\CLI; +use Chevere\Console\Console; +use Chevere\Api\Api; +use Chevere\Message; +use Chevere\Controller\Controller; +use InvalidArgumentException; + +/** + * Exposes API endpoint options. + */ +final class OptionsController extends Controller +{ + protected static $description = 'Retrieve endpoint OPTIONS.'; + + /** @var string */ + private $path; + + /** @var string */ + private $endpoint; + + public function __invoke() + { + $route = $this->app->route(); + if (isset($route)) { + $path = $route->path(); + } + if (!isset($path)) { + $this->handleError(); + + return; + } + $this->path = $path; + $this->endpoint = ltrim($this->path, '/'); + $this->process(); + } + + private function handleError() + { + $this->app->response()->setStatusCode(400); + $msg = 'Must provide a %s argument when running this callable without route context.'; + $message = (new Message($msg))->code('%s', '$path')->toString(); + if (CLI) { + Console::cli()->style()->error($message); + + return; + } else { + throw new InvalidArgumentException($message); + } + } + + private function process() + { + $statusCode = 200; + $endpoint = $this->app->api()->endpoint($this->endpoint); + if ($endpoint['OPTIONS']) { + $this->app->response()->addData('OPTIONS', $this->path, $endpoint['OPTIONS']); + } else { + $statusCode = 404; + // $json->setResponse("Endpoint doesn't exists", $statusCode); + } + $this->app->response()->setStatusCode($statusCode); + } +} diff --git a/Chevereto-Chevere/src/Controllers/HeadController.php b/Chevereto-Chevere/src/Controllers/HeadController.php new file mode 100644 index 000000000..f4f4de3fd --- /dev/null +++ b/Chevereto-Chevere/src/Controllers/HeadController.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Controllers; + +use const Chevere\CLI; +use Chevere\Console\Console; +use Chevere\Controller\Controller; + +final class HeadController extends Controller +{ + const OPTIONS = [ + 'description' => 'GETT without message-body.', + ]; + + /** + * Head takes the URI and invokes GET. + */ + public function __invoke() + { + $route = $this->app->route(); + $controller = $route->getController('GET'); + if ($controller) { + $this->invoke($controller); + $this->app->response()->setContent(null); + if (CLI) { + Console::cli()->style()->block($this->app->response()->getStatusString(), 'STATUS', 'fg=black;bg=green', ' ', true); + } + } + } +} diff --git a/Chevereto-Chevere/src/Data/Data.php b/Chevereto-Chevere/src/Data/Data.php new file mode 100644 index 000000000..9571950f9 --- /dev/null +++ b/Chevereto-Chevere/src/Data/Data.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Data; + +use ArrayIterator; +use Chevere\Contracts\DataContract; + +/** + * Data wrapper. + */ +class Data implements DataContract +{ + /** @var array */ + private $data; + + public function __construct() + { + $this->data = []; + } + + public static function fromArray(array $data): DataContract + { + $that = new self(); + $that->data = $data; + + return $that; + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->data); + } + + public function count(): int + { + return count($this->data); + } + + public function set(array $data): DataContract + { + $this->data = $data; + + return $this; + } + + public function merge(array $data): DataContract + { + if (isset($this->data)) { + $data = array_merge_recursive($this->data, $data); + } + + return $this->set($data); + } + + public function append($var): DataContract + { + $this->data[] = $var; + + return $this; + } + + public function get(): ?array + { + return $this->data; + } + + public function toArray(): array + { + return $this->data ?? []; + } + + public function hasKey(string $key): bool + { + return array_key_exists($key, $this->data); + } + + public function setKey(string $key, $var): DataContract + { + $this->data[$key] = $var; + + return $this; + } + + public function getKey(string $key) + { + return $this->data[$key] ?? null; + } + + public function removeKey(string $key): DataContract + { + unset($this->data[$key]); + + return $this; + } +} diff --git a/Chevereto-Chevere/src/Data/Traits/DataAccessTrait.php b/Chevereto-Chevere/src/Data/Traits/DataAccessTrait.php new file mode 100644 index 000000000..63b9ae1cd --- /dev/null +++ b/Chevereto-Chevere/src/Data/Traits/DataAccessTrait.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Data\Traits; + +use Chevere\Data\Data; +use Chevere\Contracts\DataContract; + +trait DataAccessTrait +{ + /** @var DataContract */ + private $data; + + public function data(): DataContract + { + return $this->data; + } +} diff --git a/Chevereto-Chevere/src/Data/Traits/DataKeyTrait.php b/Chevereto-Chevere/src/Data/Traits/DataKeyTrait.php new file mode 100644 index 000000000..22f189e64 --- /dev/null +++ b/Chevereto-Chevere/src/Data/Traits/DataKeyTrait.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Data\Traits; + +use Chevere\Data\Data; +use Chevere\Contracts\DataContract; + +trait DataKeyTrait +{ + /** @var DataContract */ + private $data; + + public function dataKey(string $key) + { + return $this->data->getKey($key); + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/ErrorHandler.php b/Chevereto-Chevere/src/ExceptionHandler/ErrorHandler.php new file mode 100644 index 000000000..99e9158c6 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/ErrorHandler.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler; + +use ErrorException; + +/** + * The Chevere errors-as-exception handler. + */ +final class ErrorHandler +{ + public static function error($severity, $message, $file, $line): void + { + throw new ErrorException($message, 0, $severity, $file, $line); + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/ExceptionHandler.php b/Chevereto-Chevere/src/ExceptionHandler/ExceptionHandler.php new file mode 100644 index 000000000..5b4e25a27 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/ExceptionHandler.php @@ -0,0 +1,223 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler; + +use const Chevere\APP_PATH; + +use DateTime; +use DateTimeZone; +use Throwable; +use Chevere\Http\Request; +use Chevere\App\Loader; +use Chevere\Data\Data; +use Chevere\Path\Path; +use Chevere\Runtime\Runtime; +use Chevere\ExceptionHandler\src\Formatter; +use Chevere\ExceptionHandler\src\Output; +use Chevere\ExceptionHandler\src\Style; +use Chevere\ExceptionHandler\src\Template; +use Chevere\ExceptionHandler\src\Wrap; +use Psr\Log\LogLevel; +use Monolog\Logger; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\StreamHandler; +use Monolog\Handler\FirePHPHandler; +use Chevere\Contracts\DataContract; +use Chevere\Data\Traits\DataAccessTrait; +use Chevere\Data\Traits\DataKeyTrait; + +/** + * The Chevere exception handler. + */ +final class ExceptionHandler +{ + use DataAccessTrait; + use DataKeyTrait; + + /** @var string Relative folder where logs will be stored */ + const LOG_DATE_FOLDER_FORMAT = 'Y/m/d/'; + + /** @var ?bool Null will read app/config.php. Any boolean value will override that */ + const DEBUG = null; + + /** @var string Null will use App\PATH_LOGS ? PATH_LOGS ? traverse */ + const PATH_LOGS = APP_PATH . 'var/logs/'; + + /** Readable PHP error mapping */ + const ERROR_TABLE = [ + E_ERROR => 'Fatal error', + E_WARNING => 'Warning', + E_PARSE => 'Parse error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core error', + E_CORE_WARNING => 'Core warning', + E_COMPILE_ERROR => 'Compile error', + E_COMPILE_WARNING => 'Compile warning', + E_USER_ERROR => 'Fatal error', + E_USER_WARNING => 'Warning', + E_USER_NOTICE => 'Notice', + E_STRICT => 'Strict standars', + E_RECOVERABLE_ERROR => 'Recoverable error', + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'Deprecated', + ]; + + /** PHP error code LogLevel table. Taken from Monolog\ErrorHandler (defaultErrorLevelMap). */ + const PHP_LOG_LEVEL = [ + E_ERROR => LogLevel::CRITICAL, + E_WARNING => LogLevel::WARNING, + E_PARSE => LogLevel::ALERT, + E_NOTICE => LogLevel::NOTICE, + E_CORE_ERROR => LogLevel::CRITICAL, + E_CORE_WARNING => LogLevel::WARNING, + E_COMPILE_ERROR => LogLevel::ALERT, + E_COMPILE_WARNING => LogLevel::WARNING, + E_USER_ERROR => LogLevel::ERROR, + E_USER_WARNING => LogLevel::WARNING, + E_USER_NOTICE => LogLevel::NOTICE, + E_STRICT => LogLevel::NOTICE, + E_RECOVERABLE_ERROR => LogLevel::ERROR, + E_DEPRECATED => LogLevel::NOTICE, + E_USER_DEPRECATED => LogLevel::NOTICE, + ]; + + /** @var DataContract */ + private $data; + + /** @var Request The detected/forged HTTP request */ + private $request; + + /** @var bool */ + private $isDebugEnabled; + + /** @var string */ + private $loggerLevel; + + /** @var Wrap */ + private $wrap; + + /** @var string */ + private $logDateFolderFormat; + + private $logger; + + /** @var Runtime */ + private $runtime; + + /** @var array Contains all the loaded configuration files (App) */ + // private $loadedConfigFiles; + + /** @var Output */ + private $output; + + /** + * @param mixed $args Arguments passed to the error exception (severity, message, file, line; Exception) + */ + public function __construct(...$args) + { + $this->data = new Data(); + $this->setTimeProperties(); + $this->data->setkey('id', uniqid('', true)); + try { + $this->request = Loader::request(); + } catch (Throwable $e) { + $this->request = Request::createFromGlobals(); + } + $this->runtime = Loader::runtime(); + $this->isDebugEnabled = (bool) $this->runtime->dataKey('debug'); + + $this->setloadedConfigFiles(); + + $this->logDateFolderFormat = static::LOG_DATE_FOLDER_FORMAT; + $this->wrap = new Wrap($args[0]); + $this->loggerLevel = $this->wrap->dataKey('loggerLevel'); + $this->setLogFilePathProperties(); + $this->setLogger(); + + $formatter = new Formatter($this); + $formatter->setLineBreak(Template::BOX_BREAK_HTML); + $formatter->setCss(Style::CSS); + + $this->output = new Output($this, $formatter); + $this->loggerWrite(); + $this->output->out(); + } + + public function isDebugEnabled(): bool + { + return $this->isDebugEnabled; + } + + public function request(): Request + { + return $this->request; + } + + public function wrap(): Wrap + { + return $this->wrap; + } + + public static function exception($e): void + { + new static($e); + } + + private function setTimeProperties(): void + { + $dt = new DateTime('now', new DateTimeZone('UTC')); + $dateTimeAtom = $dt->format(DateTime::ATOM); + $this->data->merge([ + 'dateTimeAtom' => $dateTimeAtom, + 'timestamp' => strtotime($dateTimeAtom), + ]); + } + + private function setloadedConfigFiles(): void + { + //FIXME: + // $this->loadedConfigFiles = $this->runtime->getRuntimeConfig()->getLoadedFilepaths(); + // $this->data->setKey('loadedConfigFilesString', implode(';', $this->loadedConfigFiles)); + } + + private function setLogFilePathProperties(): void + { + $path = Path::normalize(static::PATH_LOGS); + $path = rtrim($path, '/') . '/'; + $date = gmdate($this->logDateFolderFormat, $this->data->getKey('timestamp')); + $id = $this->data->getKey('id'); + $timestamp = $this->data->getKey('timestamp'); + $logFilename = $path . $this->loggerLevel . '/' . $date . $timestamp . '_' . $id . '.log'; + $this->data->setKey('logFilename', $logFilename); + } + + private function setLogger(): void + { + $lineFormatter = new LineFormatter(null, null, true, true); + $logFilename = $this->data->getKey('logFilename'); + $streamHandler = new StreamHandler($logFilename); + $streamHandler->setFormatter($lineFormatter); + $this->logger = new Logger(__NAMESPACE__); + $this->logger->setTimezone(new DateTimeZone('UTC')); + $this->logger->pushHandler($streamHandler); + $this->logger->pushHandler(new FirePHPHandler()); + } + + private function loggerWrite(): void + { + $log = strip_tags($this->output->textPlain()); + $log .= "\n\n" . str_repeat('=', Formatter::COLUMNS); + $this->logger->log($this->loggerLevel, $log); + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/Formatter.php b/Chevereto-Chevere/src/ExceptionHandler/src/Formatter.php new file mode 100644 index 000000000..6c1392585 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/Formatter.php @@ -0,0 +1,318 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +use Throwable; +use ErrorException; +use Symfony\Component\Console\Output\OutputInterface; +use const Chevere\CLI; +use Chevere\Console\Console; +use Chevere\ExceptionHandler\ExceptionHandler; +use Chevere\VarDump\VarDump; +use Chevere\VarDump\PlainVarDump; +use Chevere\Utility\Str; +use Chevere\Contracts\DataContract; +use Chevere\Data\Traits\DataKeyTrait; + +/** + * Formats the error exception in HTML (default), console and plain text. + */ +final class Formatter +{ + use DataKeyTrait; + + /** @var string Number of fixed columns for plaintext display */ + const COLUMNS = 120; + + /** ExceptionHandler sections */ + const SECTION_TITLE = 'title'; + const SECTION_MESSAGE = 'message'; + const SECTION_ID = 'id'; + const SECTION_TIME = 'time'; + const SECTION_STACK = 'stack'; + const SECTION_CLIENT = 'client'; + const SECTION_REQUEST = 'request'; + const SECTION_SERVER = 'server'; + + /** Verbose aware console sections. */ + const CONSOLE_TABLE = [ + self::SECTION_TITLE => OutputInterface::VERBOSITY_QUIET, + self::SECTION_MESSAGE => OutputInterface::VERBOSITY_QUIET, + self::SECTION_ID => OutputInterface::VERBOSITY_NORMAL, + self::SECTION_TIME => OutputInterface::VERBOSITY_VERBOSE, + self::SECTION_STACK => OutputInterface::VERBOSITY_VERY_VERBOSE, + self::SECTION_CLIENT => OutputInterface::VERBOSITY_VERBOSE, + self::SECTION_REQUEST => false, + self::SECTION_SERVER => false, + ]; + + /** @var ExceptionHandler */ + private $exceptionHandler; + + /** @var string */ + private $lineBreak; + + /** @var array */ + private $plainContentSections; + + /** @var array */ + private $richContentSections; + + private $consoleSections; + + /** @var string */ + private $varDump; + + /** @var Wrap */ + private $wrap; + + /** @var Throwable */ + private $exception; + + /** @var DataContract */ + private $data; + + public function __construct(ExceptionHandler $exceptionHandler) + { + $this->varDump = VarDump::RUNTIME; + $this->exceptionHandler = $exceptionHandler; + $this->wrap = $this->exceptionHandler->wrap(); + $this->exception = $this->wrap->exception(); + $this->data = $this->wrap->data(); + $this->setServerProperties(); + $this->data->merge([ + 'thrown' => $this->wrap->dataKey('className') . ' thrown', + ]); + $this->processStack(); + $this->processContentSections(); + $this->processContentGlobals(); + $this->data->merge([ + 'title' => $this->data->getKey('thrown'), + 'bodyClass' => !headers_sent() ? 'body--flex' : 'body--block', + ]); + } + + public function setLineBreak(string $lineBreak) + { + $this->lineBreak = $lineBreak; + } + + public function setCss(string $css) + { + $this->data->setKey('css', $css); + } + + public function plainContentSections(): array + { + return $this->plainContentSections; + } + + public function richContentSections(): array + { + return $this->richContentSections; + } + + public function consoleSections(): array + { + return $this->consoleSections; + } + + public function getTemplateTags(): array + { + return [ + '%id%' => $this->exceptionHandler->dataKey('id'), + '%datetimeUtc%' => $this->exceptionHandler->dataKey('dateTimeAtom'), + '%timestamp%' => $this->exceptionHandler->dataKey('timestamp'), + '%loadedConfigFilesString%' => $this->exceptionHandler->dataKey('loadedConfigFilesString'), + '%logFilename%' => $this->exceptionHandler->dataKey('logFilename'), + '%css%' => $this->data->getKey('css'), + '%bodyClass%' => $this->data->getKey('bodyClass'), + '%body%' => null, + '%title%' => $this->data->getKey('title'), + '%content%' => null, + '%title%' => $this->data->getKey('title'), + '%file%' => $this->data->getKey('file'), + '%line%' => $this->data->getKey('line'), + '%message%' => $this->data->getKey('message'), + '%code%' => $this->data->getKey('code'), + '%plainStack%' => $this->data->getKey('plainStack'), + '%consoleStack%' => $this->data->getKey('consoleStack'), + '%richStack%' => $this->data->getKey('richStack'), + '%clientIp%' => $this->data->getKey('clientIp'), + '%clientUserAgent%' => $this->data->getKey('clientUserAgent'), + '%serverProtocol%' => $this->data->getKey('serverProtocol'), + '%requestMethod%' => $this->data->getKey('requestMethod'), + '%uri%' => $this->data->getKey('uri') ?? null, + '%serverHost%' => $this->data->getKey('serverHost'), + '%serverPort%' => $this->data->getKey('serverPort'), + '%serverSoftware%' => $this->data->getKey('serverSoftware'), + ]; + } + + private function setServerProperties() + { + if (CLI) { + $this->data->merge([ + 'clientIp' => $_SERVER['argv'][0], + 'clientUserAgent' => Console::inputString(), + ]); + } else { + $this->data->merge([ + 'uri' => $this->exceptionHandler->request()->getRequestUri() ?? 'unknown', + 'clientUserAgent' => $this->exceptionHandler->request()->headers->get('User-Agent'), + 'requestMethod' => $this->exceptionHandler->request()->getMethod(), + 'serverHost' => $this->exceptionHandler->request()->getHost(), + 'serverPort' => (int) $this->exceptionHandler->request()->getPort(), + 'serverProtocol' => $this->exceptionHandler->request()->getProtocolVersion(), + 'serverSoftware' => $this->exceptionHandler->request()->headers->get('SERVER_SOFTWARE'), + 'clientIp' => $this->exceptionHandler->request()->getClientIp(), + ]); + } + } + + private function processStack() + { + $trace = $this->wrap->exception()->getTrace(); + if ($this->wrap->exception() instanceof ErrorException) { + $this->data->setKey('thrown', $this->wrap->dataKey('type')); + unset($trace[0]); + } + $stack = new Stack($trace); + if (CLI) { + $this->data->setKey('consoleStack', $stack->getConsole()); + } + $this->data->setKey('richStack', $stack->getRich()); + $this->data->setKey('plainStack', $stack->getPlain()); + } + + private function processContentSections() + { + $sections = [ + static::SECTION_TITLE => ['%title% in %file%:%line%'], + static::SECTION_MESSAGE => ['# Message', '%message%' . ($this->wrap->dataKey('code') ? ' [Code #%code%]' : null)], + static::SECTION_TIME => ['# Time', '%datetimeUtc% [%timestamp%]'], + static::SECTION_ID => ['# Incident ID:%id%', 'Logged at %logFilename%'], + static::SECTION_STACK => ['# Stack trace', '%plainStack%'], + static::SECTION_CLIENT => ['# Client', '%clientIp% %clientUserAgent%'], + static::SECTION_REQUEST => ['# Request', '%serverProtocol% %requestMethod% %uri%'], + static::SECTION_SERVER => ['# Server', '%serverHost% (port:%serverPort%) %serverSoftware%'], + ]; + + if (CLI) { + $verbosity = Console::cli()->output()->getVerbosity(); + } + $this->buildContentSections($sections, $verbosity ?? null); + } + + private function buildContentSections(array $sections, ?int $verbosity) + { + foreach ($sections as $k => $v) { + if (CLI && false == static::CONSOLE_TABLE[$k]) { + continue; + } + if (false === $this->processContentSectionsArray((string) $k, $v, $verbosity)) { + continue; + } + } + } + + private function processContentSectionsArray(string $key, array $value, ?int $verbosity): bool + { + $this->setPlainContentSection($key, $value); + if (isset($verbosity)) { + $verbosityLevel = static::CONSOLE_TABLE[$key]; + if (false === $verbosityLevel || $verbosity < $verbosityLevel) { + return false; + } + $this->handleSetConsoleStackSection($key, $value); + $this->setConsoleSection($key, $value); + } else { + $this->handleSetRichStackSection($key, $value); + $this->setRichContentSection($key, $value); + } + + return true; + } + + private function handleSetRichStackSection(string $key, array &$value) + { + if ($key == static::SECTION_STACK) { + $value[1] = '%richStack%'; + } + } + + private function handleSetConsoleStackSection(string $key, array &$value) + { + if ($key == static::SECTION_STACK) { + $value[1] = '%consoleStack%'; + } + } + + private function processContentGlobals() + { + foreach (['GET', 'POST', 'FILES', 'COOKIE', 'SESSION', 'SERVER'] as $v) { + $k = '_' . $v; + $v = isset($GLOBALS[$k]) ? $GLOBALS[$k] : null; + if ($v) { + $wrapped = $this->varDump::out($v); + if (!CLI) { + $wrapped = '
' . $wrapped . '
'; + } + $this->setRichContentSection($k, ['$' . $k, $this->wrapStringHr($wrapped)]); + $this->setPlainContentSection($k, ['$' . $k, strip_tags($this->wrapStringHr(PlainVarDump::out($v)))]); + } + } + } + + /** + * @param string $key content section key + * @param array $section section content [title, content] + */ + private function setPlainContentSection(string $key, array $section): void + { + $this->plainContentSections[$key] = $section; + } + + /** + * @param string $key console section key + * @param array $section section content [title, ] + */ + private function setConsoleSection(string $key, array $section): void + { + $section = array_map(function (string $value) { + return strip_tags(html_entity_decode($value)); + }, $section); + $this->consoleSections[$key] = $section; + } + + /** + * @param string $key content section key + * @param array $section section content [title, content] + */ + private function setRichContentSection(string $key, array $section): void + { + $section[0] = Str::replaceFirst('# ', '# ', $section[0]); + $this->richContentSections[$key] = $section; + } + + /** + * @param string $text text to wrap + * + * @return string wrapped text + */ + private function wrapStringHr(string $text): string + { + return $this->lineBreak . "\n" . $text . "\n" . $this->lineBreak; + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/Output.php b/Chevereto-Chevere/src/ExceptionHandler/src/Output.php new file mode 100644 index 000000000..02ec9b428 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/Output.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +use const Chevere\CLI; +use DateTime; +use Symfony\Component\HttpFoundation\Response as HttpResponse; +use Symfony\Component\HttpFoundation\JsonResponse as HttpJsonResponse; +use JakubOnderka\PhpConsoleColor\ConsoleColor; +use Chevere\Console\Console; +use Chevere\ExceptionHandler\ExceptionHandler; +use Chevere\Json; +use Chevere\Message; + +/** + * Provides ExceptionHandler output by passing a Formatter. FIXME: Don't handle responses! + */ +final class Output +{ + /** @var string The console|html content representation */ + private $content; + + /** @var string The text/plain content representation */ + private $textPlain; + + /** @var array */ + private $templateTags; + + /** @var ExceptionHandler */ + private $exceptionHandler; + + /** @var Formatter */ + private $formatter; + + /** @var string */ + private $output; + + /** @var array */ + private $headers = []; + + /** @var string The rich template string. Note: Placeholders won't be visible when dumping to console */ + private $richTemplate; + + /** @var string The plain template string. */ + private $plainTemplate; + + public function __construct(ExceptionHandler $exceptionHandler, Formatter $formatter) + { + $this->exceptionHandler = $exceptionHandler; + $this->formatter = $formatter; + $this->generateTemplates(); + $this->parseTemplates(); + if ($exceptionHandler->request()->isXmlHttpRequest()) { + $this->setJsonOutput(); + } else { + if (CLI) { + $this->setConsoleOutput(); + } else { + $this->setHtmlOutput(); + } + } + } + + public function textPlain(): string + { + return $this->textPlain; + } + + public function out(): void + { + if ($this->exceptionHandler->request()->isXmlHttpRequest()) { + $response = new HttpJsonResponse(); + } else { + $response = new HttpResponse(); + } + $response->setContent($this->output); + $response->setLastModified(new DateTime()); + $response->setStatusCode(500); + foreach ($this->headers as $k => $v) { + $response->headers->set($k, $v); + } + $response->send(); + } + + private function parseTemplates(): void + { + $this->templateTags = $this->formatter->getTemplateTags(); + $this->content = strtr($this->richTemplate, $this->templateTags); + $this->addTemplateTag('content', $this->content); + $this->textPlain = strtr($this->plainTemplate, $this->templateTags); + } + + /** + * $table stores the template placeholders and its value. + * + * @param string $tagName Template tag name + * @param mixed $value value + */ + private function addTemplateTag(string $tagName, $value): void + { + $this->templateTags["%$tagName%"] = $value; + } + + /** + * @param string $tagName Template tag name + */ + private function getTemplateTag(string $tagName): string + { + return $this->templateTags["%$tagName%"]; + } + + private function setJsonOutput(): void + { + $json = new Json(); + $this->headers = array_merge($this->headers, Json::CONTENT_TYPE); + $response = [Template::NO_DEBUG_TITLE_PLAIN, 500]; + $log = [ + 'id' => $this->getTemplateTag('id'), + 'level' => $this->formatter->dataKey('loggerLevel'), + 'filename' => $this->getTemplateTag('logFilename'), + ]; + switch ($this->exceptionHandler->isDebugEnabled()) { + case 0: + unset($log['filename']); + break; + case 1: + $response[0] = $this->formatter->dataKey('thrown').' in '.$this->getTemplateTag('file').':'.$this->getTemplateTag('line'); + $error = []; + foreach (['file', 'line', 'code', 'message', 'class'] as $v) { + $error[$v] = $this->getTemplateTag($v); + } + $json->data->setKey('error', $error); + break; + } + $json->data->setKey('log', $log); + $json->setResponse(...$response); + $this->output = (string) $json; + } + + private function setHtmlOutput(): void + { + if ($this->exceptionHandler->isDebugEnabled()) { + $bodyTemplate = Template::DEBUG_BODY_HTML; + } else { + $this->content = Template::NO_DEBUG_CONTENT_HTML; + $this->addTemplateTag('content', $this->content); + $this->addTemplateTag('title', Template::NO_DEBUG_TITLE_PLAIN); + $bodyTemplate = Template::NO_DEBUG_BODY_HTML; + } + $this->addTemplateTag('body', strtr($bodyTemplate, $this->templateTags)); + $this->output = strtr(Template::HTML_TEMPLATE, $this->templateTags); + } + + private function setConsoleOutput(): void + { + foreach ($this->formatter->consoleSections() as $k => $v) { + if ('title' == $k) { + $tpl = $v[0]; + } else { + Console::cli()->style()->section(strtr($v[0], $this->templateTags)); + $tpl = $v[1]; + } + $message = strtr($tpl, $this->templateTags); + if ('title' == $k) { + Console::cli()->style()->error($message); + } else { + $message = (new Message($message))->toCliString(); + Console::cli()->style()->writeln($message); + } + } + Console::cli()->style()->writeln(''); + } + + private function generateTemplates(): void + { + $templateStrings = new TemplatedStrings($this->formatter); + $this->richTemplate = $templateStrings->rich(); + $this->plainTemplate = $templateStrings->plain(); + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/Stack.php b/Chevereto-Chevere/src/ExceptionHandler/src/Stack.php new file mode 100644 index 000000000..cb60c620d --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/Stack.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +use const Chevere\CLI; + +/** + * Handles the ExceptionHandler exception stack trace. + */ +final class Stack +{ + /** @var array */ + private $rich; + + /** @var array */ + private $plain; + + /** @var array */ + private $console; + + private $hr = Template::BOX_BREAK_HTML; + + /** + * @param array $trace An Exception trace + */ + public function __construct(array $trace) + { + foreach ($trace as $k => $entry) { + $traceEntry = new TraceEntry($entry, $k); + if (CLI) { + $this->console[] = strtr(Template::STACK_ITEM_CONSOLE, $traceEntry->rich()); + } + $this->plain[] = strtr(Template::STACK_ITEM_HTML, $traceEntry->plain()); + $this->rich[] = strtr(Template::STACK_ITEM_HTML, $traceEntry->rich()); + } + } + + public function getConsole(): ?string + { + return strip_tags($this->wrapStringHr($this->glueString($this->console))); + } + + public function getRich(): ?string + { + return $this->wrapStringHr($this->glueString($this->rich)); + } + + public function getPlain(): ?string + { + return $this->wrapStringHr($this->glueString($this->plain)); + } + + private function glueString(array $array) + { + return implode("\n" . $this->hr . "\n", $array); + } + + private function wrapStringHr(string $text): string + { + return $this->hr . "\n" . $text . "\n" . $this->hr; + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/Style.php b/Chevereto-Chevere/src/ExceptionHandler/src/Style.php new file mode 100644 index 000000000..215990831 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/Style.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +// FIXME: Code font (inline) must be smaller +/** + * Stores the styling (CSS) for ExceptionHandler. + */ +class Style +{ + const CSS = 'html{color:#000;font:16px Helvetica,Arial,sans-serif;line-height:1.3;background:#3498db;background:-moz-linear-gradient(top,#3498db 0%,#8e44ad 100%);background:-webkit-linear-gradient(top,#3498db 0%,#8e44ad 100%);background:linear-gradient(to bottom,#3498db 0%,#8e44ad 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#3498db",endColorstr="#8e44ad",GradientType=0)}.body--block{margin:20px}.body--flex{margin:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.user-select-none{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}main{background:none;display:block;padding:0;margin:0;border:0;width:470px}.body--block main{margin:0 auto}@media (min-width:768px){main{padding:20px}}.main--stack{width:100%;max-width:900px}.hr{display:block;height:1px;color:transparent;background:hsl(192,15%,84%)}.hr>span{opacity:0;line-height:0}.main--stack hr:last-of-type{margin-bottom:0}.t{font-weight:700;margin-bottom:5px}.t--scream{font-size:2.25em;margin-bottom:0}.t--scream span{font-size:.667em;font-weight:400}.t--scream span::before{white-space:pre;content:"\A"}.t>.hide{display:inline-block}.c code{padding:2px 5px}.c code,.c pre{background:hsl(192,15%,95%);line-height:normal}.c pre.pre--even{background:hsl(192,15%,97%)}.c pre{overflow:auto;word-wrap:break-word;font-size:13px;font-family:Consolas,monospace,sans-serif;display:block;margin:0;padding:10px}main>div{padding:20px;background:#FFF}main>div,main>div> *{word-break:break-word;white-space:normal}@media (min-width:768px){main>div{-webkit-box-shadow:2px 2px 4px 0 rgba(0,0,0,.09);box-shadow:2px 2px 4px 0 rgba(0,0,0,.09);border-radius:2px}}main>div>:first-child{margin-top:0}main>div>:last-child{margin-bottom:0}.note{margin:1em 0}.fine-print{color:#BBB}.hide{width:0;height:0;opacity:0;overflow:hidden} + .c pre { + border: 1px solid hsl(192,15%,84%); + border-bottom: 0; + border-top: 0; + }'; +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/Template.php b/Chevereto-Chevere/src/ExceptionHandler/src/Template.php new file mode 100644 index 000000000..31a4fff37 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/Template.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +/** + * Stores the template strings used by ExceptionHandler. + */ +class Template +{ + /** @var string Title used when debug is disabled (App config) */ + const NO_DEBUG_TITLE_PLAIN = 'Something went wrong'; + + /** @var string HTML content used when debug is disabled (App config) */ + const NO_DEBUG_CONTENT_HTML = '

The system has failed and the server wasn\'t able to fulfil your request. This incident has been logged.

Please try again later and if the problem persist don\'t hesitate to contact your system administrator.

'; + + /** + * Stack placeholders (STACK_ITEM_HTML, STACK_ITEM_CONSOLE) + * - %x% Applies even class (pre--even) + * - %i% Stack number + * - %f% File + * - %l% Line + * - %fl% File + Line + * - %c% class + * - %t% type (::, ->) + * - %m% Method (function) + * - %a% Arguments. + */ + + /** @var string HTML template used for each stack entry */ + const STACK_ITEM_HTML = "
#%i% %fl%\n%c%%t%%m%()%a%
"; + + /** @var string Console template used for each stack entry */ + const STACK_ITEM_CONSOLE = "#%i% %fl%\n%c%%t%%m%()%a%"; + + /** + * HTML placeholders (HTML_TEMPLATE, NO_DEBUG_BODY_HTML, DEBUG_BODY_HTML, BOX_BREAK_HTML). + * + * @see Formatter::parseTemplate + */ + + /** @var string HTML template (whole document) */ + const HTML_TEMPLATE = '%body%'; + + /** @var string HTML body used when debug is disabled (App config) */ + const NO_DEBUG_BODY_HTML = '
%title%
%content%

%datetimeUtc% • %id%

'; + + /** @var string HTML body used when debug is enabled (App config) */ + const DEBUG_BODY_HTML = '
%content%
Note: This message is being displayed because of active debug mode. Remember to turn this off when going production by editing %loadedConfigFilesString%
'; + const BOX_BREAK_HTML = '
------------------------------------------------------------
'; +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/TemplatedStrings.php b/Chevereto-Chevere/src/ExceptionHandler/src/TemplatedStrings.php new file mode 100644 index 000000000..8a531a8c7 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/TemplatedStrings.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +/** + * Generates template strings for Output. + */ +final class TemplatedStrings +{ + /** @var string */ + private $titleBreak; + + /** @var array */ + private $richSection; + + /** @var array */ + private $plainSection; + + /** @var int */ + private $sectionLength; + + /** @var int */ + private $sectionsLength; + + /** @var int */ + private $i; + + /** @var string */ + private $rich; + + /** @var string */ + private $plain; + + public function __construct(Formatter $formatter) + { + $this->rich = ''; + $this->plain = ''; + $this->sectionsLength = count($formatter->plainContentSections()); + $this->titleBreak = str_repeat('=', $formatter::COLUMNS); + $this->i = 0; + foreach ($formatter->plainContentSections() as $k => $plainSection) { + $this->plainSection = $plainSection; + $this->sectionLength = count($plainSection); + $richSection = $formatter->richContentSections()[$k] ?? null; + if ($richSection) { + $this->richSection = $richSection; + } + $this->process(); + ++$this->i; + } + } + + public function rich(): string + { + return $this->rich; + } + + public function plain(): string + { + return $this->plain; + } + + private function process(): void + { + $this->appendSectionWrap(); + $this->appendSectionContents(); + if ($this->i + 1 < $this->sectionsLength) { + $this->appendRichSectionBreak(); + $this->appendPlainSectionBreak(); + } + } + + private function appendSectionContents(): void + { + if ($this->i > 0) { + $j = 1 == $this->sectionLength ? 0 : 1; + for ($j; $j < $this->sectionLength; ++$j) { + if ($this->sectionLength > 1) { + $this->appendEOL(); + } + $this->rich .= '
'.$this->richSection[$j].'
'; + $this->plain .= $this->plainSection[$j]; + } + } + } + + private function appendSectionWrap(): void + { + if (0 == $this->i || isset($this->plainSection[1])) { + $this->rich .= '
'.$this->richSection[0].'
'; + $this->plain .= html_entity_decode($this->plainSection[0]); + if (0 == $this->i) { + $this->appendRichTitleBreak(); + $this->appendPlainTitleBreak(); + } + } + } + + private function appendRichTitleBreak(): void + { + $this->rich .= "\n".'
'.$this->titleBreak.'
'; + } + + private function appendPlainTitleBreak(): void + { + $this->plain .= "\n".$this->titleBreak; + } + + private function appendEOL(): void + { + $this->rich .= "\n"; + $this->plain .= "\n"; + } + + private function appendRichSectionBreak(): void + { + $this->rich .= "\n".'
'."\n"; + } + + private function appendPlainSectionBreak(): void + { + $this->plain .= "\n\n"; + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/TraceEntry.php b/Chevereto-Chevere/src/ExceptionHandler/src/TraceEntry.php new file mode 100644 index 000000000..699b88775 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/TraceEntry.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +use ReflectionMethod; +use const Chevere\PATH; +use Chevere\App\App; +use Chevere\Path\Path; +use Chevere\Utility\Str; +use Chevere\VarDump\VarDump; +use Chevere\VarDump\PlainVarDump; + +/** + * TraceEntry prepares the exception trace for being used with Stack. + */ +final class TraceEntry +{ + /** @var array Exception trace entry */ + private $entry; + + /** @var int Key for the passed trace entry */ + private $key; + + /** @var string Plain representation of the entry arguments */ + private $plainArgs; + + /** @var string Rich representation of the entry arguments (colored) */ + private $richArgs; + + /** @var string */ + private $varDump; + + /** @var array */ + private $rich; + + /** @var array */ + private $plain; + + public function __construct(array $entry, int $key) + { + $this->entry = $entry; + $this->key = $key; + $this->varDump = VarDump::RUNTIME; + $this->handleProcessMissingClassFile(); + $this->handleSetEntryArguments(); + $this->handleProcessAnonClass(); + $this->handleProcessCoreAutoloader(); + $this->handleProcessNormalizeFile(); + $this->setPlain(); + $this->setRich(); + } + + public function rich(): array + { + return $this->rich; + } + + public function plain(): array + { + return $this->plain; + } + + private function setPlain(): void + { + $this->plain = [ + '%x%' => ($this->key & 1) ? 'pre--even' : null, + '%i%' => $this->key, + '%f%' => $this->entry['file'] ?? null, + '%l%' => $this->entry['line'] ?? null, + '%fl%' => isset($this->entry['file']) ? ($this->entry['file'].':'.$this->entry['line']) : null, + '%c%' => $this->entry['class'] ?? null, + '%t%' => $this->entry['type'] ?? null, + '%m%' => $this->entry['function'], + '%a%' => $this->plainArgs ?? null, + ]; + } + + private function setRich(): void + { + $this->rich = $this->plain; + array_pop($this->rich); + foreach ([ + '%f%' => VarDump::_FILE, + '%l%' => VarDump::_FILE, + '%fl%' => VarDump::_FILE, + '%c%' => VarDump::_CLASS, + '%t%' => VarDump::_OPERATOR, + '%m%' => VarDump::_FUNCTION, + ] as $k => $v) { + $wrapper = VarDump::wrap($v, (string) $this->plain[$k]); + $this->rich[$k] = isset($this->plain[$k]) ? $wrapper : null; + } + $this->rich['%a%'] = $this->richArgs; + } + + private function handleProcessMissingClassFile() + { + if (!array_key_exists('file', $this->entry) && isset($this->entry['class'])) { + $this->processMissingClassFile(); + } + } + + private function processMissingClassFile() + { + $reflector = new ReflectionMethod($this->entry['class'], $this->entry['function']); + $filename = $reflector->getFileName(); + if (false !== $filename) { + $this->entry['file'] = $filename; + $this->entry['line'] = $reflector->getStartLine(); + } + } + + private function handleSetEntryArguments() + { + if (isset($this->entry['args']) && is_array($this->entry['args'])) { + $this->setFrameArguments(); + } + } + + private function setFrameArguments() + { + $this->plainArgs = "\n"; + $this->richArgs = "\n"; + foreach ($this->entry['args'] as $k => $v) { + $aux = 'Arg#'.($k + 1).' '; + $this->plainArgs .= $aux.PlainVarDump::out($v, null, [App::class])."\n"; + $this->richArgs .= $aux.$this->varDump::out($v, null, [App::class])."\n"; + } + $this->trimTrailingNl($this->plainArgs); + $this->trimTrailingNl($this->richArgs); + } + + private function trimTrailingNl(string &$string): void + { + $string = rtrim($string, "\n"); + } + + private function handleProcessAnonClass() + { + if (isset($this->entry['class']) && Str::startsWith(VarDump::ANON_CLASS, $this->entry['class'])) { + $this->processAnonClass(); + } + } + + private function processAnonClass() + { + $entryFile = Str::replaceFirst(VarDump::ANON_CLASS, '', $this->entry['class']); + $this->entry['file'] = substr($entryFile, 0, strpos($entryFile, '.php') + 4); + $this->entry['class'] = VarDump::ANON_CLASS; + $this->entry['line'] = null; + } + + private function handleProcessCoreAutoloader() + { + if ($this->entry['function'] == 'Chevere\\autoloader') { + $this->processCoreAutoloader(); + } + } + + private function processCoreAutoloader() + { + $this->entry['file'] = $this->entry['file'] ?? (PATH.'autoloader.php'); + } + + private function handleProcessNormalizeFile() + { + if (isset($this->entry['file']) && Str::contains('\\', $this->entry['file'])) { + $this->processNormalizeFile(); + } + } + + private function processNormalizeFile() + { + $this->entry['file'] = Path::normalize($this->entry['file']); + } +} diff --git a/Chevereto-Chevere/src/ExceptionHandler/src/Wrap.php b/Chevereto-Chevere/src/ExceptionHandler/src/Wrap.php new file mode 100644 index 000000000..67575d883 --- /dev/null +++ b/Chevereto-Chevere/src/ExceptionHandler/src/Wrap.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\ExceptionHandler\src; + +use Throwable; +use ErrorException; +use Chevere\Data\Data; +use Chevere\Path\Path; +use Chevere\Utility\Str; +use Chevere\Contracts\DataContract; +use Chevere\Data\Traits\DataAccessTrait; +use Chevere\Data\Traits\DataKeyTrait; +use Chevere\ExceptionHandler\ExceptionHandler; + +/** + * Wraps throwable exception. + */ +final class Wrap +{ + use DataAccessTrait; + use DataKeyTrait; + + /** @var Throwable */ + private $exception; + + /** @var DataContract */ + private $data; + + /** @var Throwable $exception */ + public function __construct(Throwable $exception) + { + $this->exception = $exception; + $this->data = new Data(); + $className = get_class($exception); + if (Str::startsWith('Chevere\\', $className)) { + $className = Str::replaceFirst('Chevere\\', '', $className); + } + if ($exception instanceof ErrorException) { + /* @scrutinizer ignore-call */ + $phpCode = $exception->getSeverity(); + $code = $phpCode; + $errorType = $phpCode; + } else { + $phpCode = E_ERROR; + $code = $exception->getCode(); + $errorType = $phpCode; + } + $this->data->merge([ + 'className' => $className, + 'code' => $code, + 'errorType' => $errorType, + 'type' => ExceptionHandler::ERROR_TABLE[$phpCode], + 'loggerLevel' => ExceptionHandler::PHP_LOG_LEVEL[$phpCode] ?? 'error', + 'message' => $exception->getMessage(), + 'file' => Path::normalize($exception->getFile()), + 'line' => (int) $exception->getLine(), + ]); + } + + public function exception(): Throwable + { + return $this->exception; + } +} diff --git a/Chevereto-Chevere/src/File.php b/Chevereto-Chevere/src/File.php new file mode 100644 index 000000000..418239a1f --- /dev/null +++ b/Chevereto-Chevere/src/File.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +use RuntimeException; +use Chevere\Path\Path; + +final class File +{ + /** + * Fast wat to determine if a file or directory exists using stream_resolve_include_path. + * + * @param string $filename Absolute file path + * @param bool $clearCache TRUE to call clearstatcache + * + * @return bool TRUE if the $filename exists + */ + public static function exists(string $filename, bool $clearCache = true): bool + { + if ($clearCache) { + clearstatcache(true); + } + // Only tweak relative paths, without wrappers or anything else + // Note that stream_resolve_include_path won't work with relative paths if no chdir(). + if (!Path::isAbsolute($filename)) { + $filename = Path::absolute($filename); + } + + return stream_resolve_include_path($filename) !== false; + } + + public static function put(string $filename, $contents) + { + if (!static::exists($filename)) { + $dirname = dirname($filename); + if (!static::exists($dirname)) { + Path::create($dirname); + } + } + if (false === @file_put_contents($filename, $contents)) { + throw new RuntimeException( + (new Message('Unable to write content to file %filepath%')) + ->code('%filepath%', $filename) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/FileReturn/FileReturn.php b/Chevereto-Chevere/src/FileReturn/FileReturn.php new file mode 100644 index 000000000..32cdb053b --- /dev/null +++ b/Chevereto-Chevere/src/FileReturn/FileReturn.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\FileReturn; + +use RuntimeException; +use Chevere\File; +use Chevere\Message; +use Chevere\Path\PathHandle; + +/** + * FileReturn provides an abstraction for interacting with PHP files that return a variable. + * + * ) */ + private $strict; + + public function __construct(PathHandle $pathHandle) + { + $this->strict = true; + $this->path = $pathHandle->path(); + } + + public function setStrict(bool $toggle) + { + $this->strict = $toggle; + } + + public function path(): string + { + return $this->path; + } + + public function checksum(): string + { + if (!isset($this->checksum)) { + $this->checksum = $this->getHashFile(); + } + return $this->checksum; + } + + public function contents(): string + { + if (!isset($this->contents)) { + $this->contents = file_get_contents($this->path); + } + return $this->contents; + } + + public function raw() + { + if (!isset($this->raw)) { + if (!File::exists($this->path)) { + throw new RuntimeException( + (new Message("File %filepath% doesn't exists.")) + ->code('%filepath%', $this->path) + ->toString() + ); + } + $this->validate(); + $this->raw = include $this->path; + $this->type = gettype($this->raw); + } + return $this->raw; + } + + public function type(): string + { + if (!isset($this->type)) { + $this->type = gettype($this->raw()); + } + return $this->type; + } + + /** + * Gets the content of the file appling unserialize. + * TODO: Rename to something with more context + */ + public function get() + { + if (!isset($this->var)) { + $this->var = $this->raw(); + if (is_iterable($this->var)) { + foreach ($this->var as $k => &$v) { + $this->unseralize($v); + } + } else { + $this->unseralize($this->var); + } + } + return $this->var; + } + + /** + * Put $var into the file using var_export return + */ + public function put($var) + { + if (is_iterable($var)) { + foreach ($var as $k => &$v) { + $this->switchVar($v); + } + } else { + $this->switchVar($var); + } + $varExport = var_export($var, true); + $export = FileReturn::PHP_RETURN . $varExport . ';'; + File::put($this->path, $export); + $this->checksum = $this->getHashFile(); + unset($this->contents); + } + + /** + * OPCache the FileReturn file + */ + public function compile() + { + opcache_compile_file($this->path); + } + + public function invalidateCache() + { + opcache_invalidate($this->path); + } + + private function getHashFile() + { + return hash_file(static::CHECKSUM_ALGO, $this->path); + } + + private function validate() + { + if ($this->strict) { + $this->validateStrict(); + } else { + $this->validateNonStrict(); + } + } + + private function validateStrict(): void + { + $handle = fopen($this->path, 'r'); + if (false === $handle) { + throw new RuntimeException( + (new Message('Unable to %fn% %filepath% in %mode% mode')) + ->code('%fn%', 'fopen') + ->code('%filepath%', $this->path) + ->code('%mode%', 'r') + ->toString() + ); + } + $contents = fread($handle, static::PHP_RETURN_CHARS); + fclose($handle); + if ($contents !== static::PHP_RETURN) { + throw new RuntimeException( + (new Message('Unexpected contents in %filepath% (strict validation)')) + ->code('%filepath%', $this->path) + ->toString() + ); + } + } + + private function validateNonStrict(): void + { + $this->contents = $this->contents(); + if (!$this->contents) { + throw new RuntimeException( + (new Message('Unable to get file %filepath% contents')) + ->code('%filepath%', $this->path) + ->toString() + ); + } + if (!preg_match_all('#<\?php([\S\s]*)\s*return\s*[\S\s]*;#', $this->contents)) { + throw new RuntimeException( + (new Message('Unexpected contents in %filepath% (non-strict validation)')) + ->code('%filepath%', $this->path) + ->toString() + ); + } + } + + private function switchVar(&$var) + { + if (is_object($var)) { + $var = serialize($var); + } + } + + private function unseralize(&$var) + { + $var = unserialize($var); + } +} diff --git a/Chevereto-Chevere/src/FromString.php b/Chevereto-Chevere/src/FromString.php new file mode 100644 index 000000000..2f161fb51 --- /dev/null +++ b/Chevereto-Chevere/src/FromString.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +abstract class FromString +{ + /** @var string Description of the string used to create object from string */ + protected static $stringDescription = 'No description'; + + /** @var string Regex used when creating object from string */ + protected static $stringRegex = '.*'; +} diff --git a/Chevereto-Chevere/src/Handler.php b/Chevereto-Chevere/src/Handler.php new file mode 100644 index 000000000..1b8cf085a --- /dev/null +++ b/Chevereto-Chevere/src/Handler.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +use App\Middleware\RoleAdmin; +use Chevere\Contracts\App\AppContract; +use Chevere\Http\Response; +use Chevere\Interfaces\HandlerInterface; +use Chevere\Interfaces\MiddlewareInterface; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; +use Throwable; + +/** + * TODO: Add stop, redirect and other methods needed to alter the flow. + */ +/** + * Handles middleware. + */ +final class Handler implements HandlerInterface +{ + /** @var AppContract */ + private $app; + + /** @var array */ + private $queue; + + /** @var bool */ + private $stopped; + + /** + * @param array $queue an array containing callables or callable strings + */ + // FIXME: Move this to another layer + public function __construct(array $queue, AppContract $app) + { + $this->app = $app; + $this->queue = $queue; + } + + // FIXME: Move this to another layer + public function runner() + { + reset($this->queue); + + return $this->handle(); + } + + public function handle(): MiddlewareInterface + { + $middleware = current($this->queue); + if ($middleware) { + next($this->queue); + + return new $middleware($this); + } + } + + public function stop(Throwable $e) + { + $this->stopped = true; + $this->exception = $e; + } +} diff --git a/Chevereto-Chevere/src/Hooking/Hook.php b/Chevereto-Chevere/src/Hooking/Hook.php new file mode 100644 index 000000000..1d5894561 --- /dev/null +++ b/Chevereto-Chevere/src/Hooking/Hook.php @@ -0,0 +1,305 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Hooking; + +use InvalidArgumentException; +use Chevere\Path\Path; +use Chevere\Message; +use Chevere\Utility\Str; + +/** + * Hooks refers to code that gets injected at determinated sections of the + * system identified by the hookable id. + * + * Hookables are identified using the following nomenclature: + * @: + * + * 1. : The named hookeable section + * 2. : Hookeable path (relative to App\PATH) + * 3. : Hookeable file_name + * + * Hooks gets registered on self::$hook following this structure: + * + * string + * => array + * => array + * => array + * => array + * callable => object (Closure) + * maker => array (size=5) + * file => + * line => + * method => + * + * A file can contain multiple anchors, which index by hookable position + * (before, after). + * + * Maker contains info about the function call used to create the hook. + */ + +// Hook::bind('myHook@controller:file', Hook::BEFORE, function ($that) { +// $that->source .= ' nosehaceeso no'; +// }); +final class Hook +{ + const ALIAS_PATH_CORE = 'core>'; + const ANCHOR = 'anchor'; + const FILE = 'file'; + const CALLABLE = 'callable'; + const MAKER = 'maker'; + const BEFORE = 'before'; + const AFTER = 'after'; + const DEFAULT_PRIORITY = 10; + + private static $hooks; + + public static function getAll() + { + return self::$hooks; + } + + /** + * Hook a hookable entry (before). Shorthand of bind(). + * + * @see bind() + * + * @param string $id hookable id + * @param callable $callable callable to run + * @param int $priority Priority in which this should be called. Lower the number, higher the priority. + * If the priority is already taken, it gets added based on inclusion order. + */ + public static function before(string $id, callable $callable, int $priority = null): void + { + self::bind($id, $callable, $priority, self::BEFORE); + } + + /** + * Hook a hookable entry (after). Shorthand of bind(). + * + * @see bind() + * + * @param string $id hookable id + * @param callable $callable callable to run + * @param int $priority Priority in which this should be called. Lower the number, higher the priority. + * If the priority is already taken, it gets added based on inclusion order. + */ + public static function after(string $id, callable $callable, int $priority = null): void + { + self::bind($id, $callable, $priority, self::AFTER); + } + + /** + * Stock hook definition in hook table (internal method). + * + * @param string $id hookable id + * @param callable $callable callable to run + * @param int $priority Priority in which this should be called. Lower the number, higher the priority. + * If the priority is already taken, it gets added based on inclusion order. + * + * @see before() + * @see after() + */ + private static function bind(string $id, callable $callable, int $priority = null, string $pos): void + { + $parsed = self::parseIdentifier($id); + extract($parsed); + $hook = [ + self::CALLABLE => $callable, + self::MAKER => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1], + ]; + $priority = $priority ?? self::DEFAULT_PRIORITY; + $f = ${self::FILE}; + $a = ${self::ANCHOR}; + $priority_exists = isset(self::$hooks[$f][$a][$pos][$priority]); + self::$hooks[$f][$a][$pos][$priority][] = $hook; + if (!$priority_exists) { + ksort(self::$hooks[$f][$a][$pos]); + } + } + + /** + * Parse hookable identifier. + * + * @param string $id Hookable identifier + * @param int $trace backtrace caller id for :path resolution + * + * @return array ['anchor' => '', 'file' => '/.php'] + */ + private static function parseIdentifier(string $id, int $trace = 3): array + { + if (Str::contains('@', $id)) { + $anchored = explode('@', $id); + $anchor = $anchored[0]; + $pathIdentifier = $anchored[1]; + } else { + $pathIdentifier = $id; + } + + return [ + self::ANCHOR => $anchor ?? null, + self::FILE => Path::fromIdentifier($pathIdentifier), + ]; + } + + /** + * Get all hooks for the given file, anchor and position. + * + * @param string $file hookeable file + * @param string $anchor hookable anchor + * @param string $pos hookable position (before, after) + * + * @return array an array containing al the callables in order + */ + public static function getAt(string $file, string $anchor, string $pos = null): array + { + if (self::$hooks == null || !isset(self::$hooks[$file])) { + return []; + } + $numArgs = func_num_args(); + switch ($numArgs) { + case 2: + return self::$hooks[$file][$anchor] ?? []; + case 3: + if (!in_array($pos, [self::BEFORE, self::AFTER])) { + throw new InvalidArgumentException( + (new Message('Invalid %s argument value, expecting %b, %a.')) + ->code('%s', '$pos') + ->code('%b', self::BEFORE) + ->code('%a', self::AFTER) + ->toString() + ); + } + + return self::$hooks[$file][$anchor][$pos] ?? []; + } + + return []; + } + + /** + * Execute all hooks for the given anchor (before and after). + * + * @param string $anchor hookable anchor + * @param callable $callable + * @param object $that that is this + * + * @see Hookeable + */ + public static function exec(string $anchor, callable $callable, object $that = null): void + { + $file = self::getCallerFile(); + $file ? self::execAt($file, $anchor, self::BEFORE, $that) : null; + $callable($that); + $file ? self::execAt($file, $anchor, self::AFTER, $that) : null; + } + + /** + * Exec all before hooks for the given anchor. + * + * @param string $anchor hookable anchor + * @param callable $callable + * @param object $that that is this + * + * @see Hookeable + */ + public static function execBefore(string $anchor, callable $callable, object $that = null): void + { + $file = self::getCallerFile(); + if (isset($file)) { + self::execAt($file, $anchor, self::BEFORE, $that); + } + $callable($that); + } + + /** + * Exec all after hooks for the given anchor. + * + * @param string $anchor hookable anchor + * @param callable $callable + * @param object $that that is this + * + * @see Hookeable + */ + public static function execAfter(string $anchor, callable $callable, object $that = null): void + { + $callable($that); + $file = self::getCallerFile(); + if (isset($file)) { + self::execAt($file, $anchor, self::AFTER, $that); + } + } + + private static function getCallerFile(): ?string + { + // 0:Hook, 1:Hookable, 3:Caller + return Path::normalize(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['file']); + } + + /** + * Execute hooks for given file, anchor and position. + * + * @param string $file hookeable file + * @param string $anchor Hookable anchor + * @param string $pos Hookable position + * @param object $that that is this + * + * @see Hookeable + */ + public static function execAt(string $file, string $anchor, string $pos, object $that = null): void + { + $hooks = self::getAt($file, $anchor, $pos); + if (!isset($hooks)) { + return; + } + foreach ($hooks as $priority => $entries) { + foreach ($entries as $entry) { + $entry[self::CALLABLE]($that); + } + } + } + + /** + * Exec before hooks for the given file and anchor. + * + * Shorthand for execAt(). + * + * @param string $file hookeable file + * @param string $anchor Hookable anchor + * @param object $that that is this + * + * @see execAt() + * @see Hookeable + */ + public static function execBeforeAt(string $file, string $anchor, object $that = null): void + { + self::execAt($file, $anchor, self::BEFORE, $that); + } + + /** + * Exec after hooks for the given file and anchor. + * + * Shorthand for execAt(). + * + * @param string $file hookeable file + * @param string $anchor Hookable anchor + * @param object $that that is this + * + * @see execAt() + * @see Hookeable + */ + public static function execAfterAt(string $file, string $anchor, object $that = null): void + { + self::execAt($file, $anchor, self::AFTER, $that); + } +} diff --git a/Chevereto-Chevere/src/Hooking/Hookable.php b/Chevereto-Chevere/src/Hooking/Hookable.php new file mode 100644 index 000000000..ad2c27e72 --- /dev/null +++ b/Chevereto-Chevere/src/Hooking/Hookable.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Hooking; + +/** + * This class provides a hookable API allowing to define anchor points where + * external code can be added on execution. + * + * For anchors defined in object context, the object itself will be passed + * throught the hookable, making possible to execute external code that + * will interact directly with the object. + * + * Your hookable entries can be defined to allow code injection before and or + * after your hookable code. + * + * Any of your classes can provide Hookeable functionality by simply extending + * this class. + * + * Pretend you have the following section inside your code: + * + * $this->prop = 'value'; + * $this->process($this->prop); + * + * That code cast process($this->prop). If you want to allow external code + * there, simly wrap the above code in something like this: + * + * $value = 'value'; + * $this->hookable('anchor', function($that) use ($value) { + * $that->prop = $value; + * }); + * $this->process($this->prop); + * + * In the above example, hooks can be registered before and after. Hooks before + * will be executed before $that->prop = $value; $that is $this. + * + * A hook should be defined like this: + * + * Hookable::after('anchor@relativePath:basename', function($that) { + * $that->prop = filter($that->prop); + * }); + * + * The object is passed directly, so the methods and properties will be + * accessible based on class visibility scope. + * + * @see Controller + * @see SimpleController + * @see Router + */ +final class Hookable +{ + /** + * Register and run hookable code entries before and after. + * + * Hook::before('anchor@relativePath:basename', function($that) { + * $that + * }); + * + * @param string $anchor hook anchor + * @param callable $callable callable + */ + public function hookable(string $anchor, callable $callable): void + { + Hook::exec($anchor, $callable, $this); + } + + /** + * Register a hookable entry before. + * + * @see hookable() + */ + public function hookableBefore(string $anchor, callable $callable): void + { + Hook::execBefore($anchor, $callable, $this); + } + + /** + * Register a hookable entry after. + * + * @see hookable() + */ + public function hookableAfter(string $anchor, callable $callable): void + { + Hook::execAfter($anchor, $callable, $this); + } + + /** + * Static version of hookable(). + * + * Static versions are limited as $this is not being passed through. + * No variable can be touched, it just adds procedures. + * + * @see hookable() + */ + public static function section(string $anchor, callable $callable): void + { + Hook::exec(...func_get_args()); + } + + /** + * Static version of hookableBefore. + * + * @see hookableBefore() + */ + public static function before(string $anchor, callable $callable): void + { + Hook::execBefore(...func_get_args()); + } + + /** + * Static version hookable after. + * + * @see hookableAfter() + */ + public static function after(string $anchor, callable $callable): void + { + Hook::execAfter(...func_get_args()); + } +} diff --git a/Chevereto-Chevere/src/Http/Http.php b/Chevereto-Chevere/src/Http/Http.php new file mode 100644 index 000000000..c092b21dc --- /dev/null +++ b/Chevereto-Chevere/src/Http/Http.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Http; + +abstract class Http +{ + const STATUS_CODES = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Reserved', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 510 => 'Not Extended', + ]; +} diff --git a/Chevereto-Chevere/src/Http/Method.php b/Chevereto-Chevere/src/Http/Method.php new file mode 100644 index 000000000..8f7304103 --- /dev/null +++ b/Chevereto-Chevere/src/Http/Method.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Http; + +use InvalidArgumentException; +use Chevere\Message; +use Chevere\Contracts\Http\MethodContract; +use Chevere\Contracts\Controller\ControllerContract; + +/** + * Api provides a static method to read the exposed API inside the app runtime. + */ +final class Method implements MethodContract +{ + /** Array containing all the known HTTP methods. */ + const ACCEPT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'COPY', 'HEAD', 'OPTIONS', 'LINK', 'UNLINK', 'PURGE', 'LOCK', 'UNLOCK', 'PROPFIND', 'VIEW', 'TRACE', 'CONNECT']; + + /** @param string HTTP request method */ + private $method; + + /** @param string ControllerContract */ + private $controller; + + public function __construct(string $method, string $controller) + { + $this->setMethod($method); + $this->setController($controller); + } + + public function method(): string + { + return $this->method; + } + + public function controller(): string + { + return $this->controller; + } + + private function setMethod(string $method) + { + if (!in_array($method, self::ACCEPT_METHODS)) { + throw new InvalidArgumentException( + (new Message('Unknown HTTP method %s.')) + ->code('%s', $method) + ->toString() + ); + } + $this->method = $method; + } + + private function setController(string $controller) + { + if (!is_subclass_of($controller, ControllerContract::class)) { + throw new InvalidArgumentException( + (new Message('Controller %s must implement the %i interface.')) + ->code('%s', $controller) + ->code('%i', ControllerContract::class) + ->toString() + ); + } + $this->controller = $controller; + } +} diff --git a/Chevereto-Chevere/src/Http/Methods.php b/Chevereto-Chevere/src/Http/Methods.php new file mode 100644 index 000000000..93dc03512 --- /dev/null +++ b/Chevereto-Chevere/src/Http/Methods.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Http; + +use ArrayIterator; +use Chevere\Contracts\Http\MethodContract; +use Chevere\Contracts\Http\MethodsContract; + +final class Methods implements MethodsContract +{ + /** @param array [MethodContract,]*/ + private $methods; + + /** @param array ['METHOD' => key,]*/ + private $index; + + public function __construct(MethodContract ...$methods) + { + foreach ($methods as $k => $method) { + $this->add($method); + } + } + + public function add(MethodContract $method): void + { + $this->methods[] = $method; + $this->index[$method->method()] = array_key_last($this->methods); + } + + public function has(string $method): bool + { + return isset($this->index[$method]); + } + + public function get(string $method): string + { + return $this->methods[$this->index[$method]]; + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->methods); + } +} diff --git a/Chevereto-Chevere/src/Http/Request.php b/Chevereto-Chevere/src/Http/Request.php new file mode 100644 index 000000000..979c5ed76 --- /dev/null +++ b/Chevereto-Chevere/src/Http/Request.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Http; + +use Chevere\Contracts\Http\Symfony\RequestContract as SymfonyRequestContract; +use Symfony\Component\HttpFoundation\Request as SymfonyRequest; + +final class Request extends SymfonyRequest implements SymfonyRequestContract +{ } diff --git a/Chevereto-Chevere/src/Http/Request/RequestException.php b/Chevereto-Chevere/src/Http/Request/RequestException.php new file mode 100644 index 000000000..d9942d8cb --- /dev/null +++ b/Chevereto-Chevere/src/Http/Request/RequestException.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Http\Request; + +use Exception; +use LogicException; +use Chevere\Http\Http; +use Chevere\Message; + +final class RequestException extends Exception +{ + public function __construct(int $code = 0, string $message = null, Exception $previous = null) + { + $status = Http::STATUS_CODES[$code]; + + if (!isset($status)) { + throw new LogicException( + (new Message('Unknown HTTP status code %code%.')) + ->code('%code%', $code) + ->toString() + ); + } + if (null == $message) { + $message = $status; + } + parent::__construct($message, $code, $previous); + } +} diff --git a/Chevereto-Chevere/src/Http/Response.php b/Chevereto-Chevere/src/Http/Response.php new file mode 100644 index 000000000..7314d7dac --- /dev/null +++ b/Chevereto-Chevere/src/Http/Response.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Http; + +use Chevere\Console\Console; +use Chevere\Contracts\Http\ResponseContract; +use Chevere\Contracts\Http\Symfony\ResponseContract as SymfonyResponseContract; +use Chevere\JsonApi\JsonApi; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; + +use const Chevere\CLI; + +/** + * Response wraps HttpFoundation response (Symfony). + */ +final class Response extends SymfonyResponse implements ResponseContract, SymfonyResponseContract +{ + /** @var string */ + protected $version; + + /** @var int */ + protected $statusCode; + + /** @var string */ + protected $statusText; + + /** @var ResponseHeaderBag */ + public $headers; + + /** + * {@inheritdoc} + */ + public function setJsonContent(JsonApi $jsonApi): void + { + $this->setJsonHeaders(); + $this->setContent($jsonApi->toString()); + } + + /** + * {@inheritdoc} + */ + public function getStatusString(): string + { + return sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText); + } + + /** + * {@inheritdoc} + */ + public function getNoBody(): string + { + return $this->getStatusString() . "\r\n" . $this->headers . "\r\n"; + } + + /** + * {@inheritdoc} + */ + public function setJsonHeaders(): void + { + if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) { + $this->headers->set('Content-Type', 'application/vnd.api+json'); + } + } + + public function send() + { + if (CLI) { + ob_start(); + parent::send(); + $this->chvBuffer = ob_get_contents(); + ob_end_clean(); + $this->chvStatus = $this->getStatusString(); + $this->chvHeaders = trim((string) $this->headers); + } else { + return parent::send(); + } + } + + public function chvStatus(): string + { + return $this->chvStatus; + } + + public function chvHeaders(): string + { + return $this->chvHeaders; + } + + public function chvBuffer(): string + { + return $this->chvBuffer; + } +} diff --git a/Chevereto-Chevere/src/Interfaces/ControllerRelationshipInterface.php b/Chevereto-Chevere/src/Interfaces/ControllerRelationshipInterface.php new file mode 100644 index 000000000..24374d795 --- /dev/null +++ b/Chevereto-Chevere/src/Interfaces/ControllerRelationshipInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Interfaces; + +interface ControllerRelationshipInterface +{ + /** + * Get the relationship property value. + */ + public static function getRelatedResource(): ?string; +} diff --git a/Chevereto-Chevere/src/Interfaces/ControllerResourceInterface.php b/Chevereto-Chevere/src/Interfaces/ControllerResourceInterface.php new file mode 100644 index 000000000..dadcf4723 --- /dev/null +++ b/Chevereto-Chevere/src/Interfaces/ControllerResourceInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Interfaces; + +interface ControllerResourceInterface +{ + /** + * Get the resource property name + */ + public static function getResourceName(): string; +} diff --git a/Chevereto-Chevere/src/Interfaces/CreateFromString.php b/Chevereto-Chevere/src/Interfaces/CreateFromString.php new file mode 100644 index 000000000..fab8aba23 --- /dev/null +++ b/Chevereto-Chevere/src/Interfaces/CreateFromString.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Interfaces; + +/** + * Provides an interface for objects that can be constructed from a string. + */ +interface CreateFromString +{ + public function createFromString(string $string): CreateFromString; +} diff --git a/Chevereto-Chevere/src/Interfaces/HandlerInterface.php b/Chevereto-Chevere/src/Interfaces/HandlerInterface.php new file mode 100644 index 000000000..33cfa2b86 --- /dev/null +++ b/Chevereto-Chevere/src/Interfaces/HandlerInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Interfaces; + +interface HandlerInterface +{ + // public function handle(); + + // public function stop(); +} diff --git a/Chevereto-Chevere/src/Interfaces/MiddlewareInterface.php b/Chevereto-Chevere/src/Interfaces/MiddlewareInterface.php new file mode 100644 index 000000000..50fd80403 --- /dev/null +++ b/Chevereto-Chevere/src/Interfaces/MiddlewareInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Interfaces; + +interface MiddlewareInterface +{ + // public function __invoke(App $app, HandlerInterface $handler); +} diff --git a/Chevereto-Chevere/src/Interfaces/PrintableInterface.php b/Chevereto-Chevere/src/Interfaces/PrintableInterface.php new file mode 100644 index 000000000..e0010dd26 --- /dev/null +++ b/Chevereto-Chevere/src/Interfaces/PrintableInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Interfaces; + +interface PrintableInterface +{ + public function __toString(): ?string; + + public function print(); + + public function exec(); +} diff --git a/Chevereto-Chevere/src/Interfaces/RenderableInterface.php b/Chevereto-Chevere/src/Interfaces/RenderableInterface.php new file mode 100644 index 000000000..46a7acf51 --- /dev/null +++ b/Chevereto-Chevere/src/Interfaces/RenderableInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Interfaces; + +interface RenderableInterface +{ + public function render(): ?string; +} diff --git a/Chevereto-Chevere/src/Json.php b/Chevereto-Chevere/src/Json.php new file mode 100644 index 000000000..4e009b315 --- /dev/null +++ b/Chevereto-Chevere/src/Json.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +use Chevere\Traits\PrintableTrait; + +final class Json implements Interfaces\PrintableInterface +{ + use PrintableTrait; + + const CODE = 'code'; + const DATA = 'data'; + const DESCRIPTION = 'description'; + const MESSAGE = 'message'; + const STATUS = 'status'; + const RESPONSE = 'response'; + const CONTENT_TYPE = ['Content-type' => 'application/json; charset=UTF-8']; + + private $response; + + private $callback; + + private $printable; + + public $content; + + /** + * JSON data constructor. + * + * @param array $data data array + */ + public function __construct() + { + // $this->data = new Data([]); + } + + /** + * Set the JSON response data. + * + * @param string $message app response message + * @param int $code app responde code + * + * @return $this chaineable + */ + public function setResponse(string $message, int $code = null): self + { + $this->response = [static::CODE => $code, static::MESSAGE => $message]; + + return $this; + } + + public function getResponse(): ?array + { + return $this->response; + } + + public function setResponseKey(string $key, $var) + { + $this->response[$key] = $var; + } + + /** + * Executes the JSON format operation. + */ + public function exec(): void + { + $output = [ + static::RESPONSE => $this->response, + ]; + $array = $this->data->toArray(); + if (isset($array)) { + $output[static::DATA] = $array; + } + $jsonEncode = json_encode($output, JSON_PRETTY_PRINT); + if (!$jsonEncode) { + $code = 500; + $output = [ + static::RESPONSE => [static::CODE => $code, static::MESSAGE => "Data couldn't be encoded into json"], + ]; + $jsonEncode = json_encode($output, JSON_PRETTY_PRINT); + } + if (!is_null($this->callback)) { + $this->printable = sprintf('%s(%s);', $this->callback, $jsonEncode); + } else { + $this->printable = $jsonEncode; + } + $this->content = $this->printable; + } +} diff --git a/Chevereto-Chevere/src/JsonApi/Data.php b/Chevereto-Chevere/src/JsonApi/Data.php new file mode 100644 index 000000000..5fd51e241 --- /dev/null +++ b/Chevereto-Chevere/src/JsonApi/Data.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\JsonApi; + +/** + * The document’s “primary data” is a representation of the resource or collection of resources targeted by a request. + * + * Primary data MUST be either: + * - a single resource object, a single resource identifier object, or null, for requests that target single resources + * - an array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections + * + * ! If a document does not contain a top-level data key, the included member MUST NOT be present either. + * ! The members data and errors MUST NOT coexist in the same document. + */ +final class Data +{ + /** @var string */ + private $type; + + /** @var string */ + private $id; + + /** @var iterable */ + private $attributes; + + public function __construct(string $type, string $id) + { + $this->type = $type; + $this->id = $id; + } + + public function addAttribute(string $name, string $data): void + { + $this->attributes[$name] = $data; + } + + public function toArray(): array + { + $return = [ + 'type' => $this->type, + 'id' => $this->id, + ]; + if (isset($this->attributes)) { + $return['attributes'] = $this->attributes; + } + return $return; + } +} diff --git a/Chevereto-Chevere/src/JsonApi/JsonApi.php b/Chevereto-Chevere/src/JsonApi/JsonApi.php new file mode 100644 index 000000000..d7428750f --- /dev/null +++ b/Chevereto-Chevere/src/JsonApi/JsonApi.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\JsonApi; + +use JsonException; +use InvalidArgumentException; +use Chevere\Message; +use const Chevere\CLI; + +final class JsonApi +{ + // Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML. + const DEFAULT_ENCODING_OPTIONS = JSON_THROW_ON_ERROR | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; + + /** @var int Bitmask */ + private $encodingOptions; + + /** @var array The document’s “primary data” */ + private $data; + + /** @var array */ + private $array; + + /** @var Errors An array of error objects */ + private $errors; + + /** @var Included An array of resource objects that are related to the primary data and/or each other (“included resources”). */ + private $included; + + /** @var array Describes the server’s implementation */ + private $jsonapi; + + /** @var Links A links object related to the primary data. */ + private $links; + + /** @var Meta A meta object that contains non-standard meta-information. */ + private $meta; + + public function __construct() + { + $this->setEncodingOptions(); + } + + public function appendData(Data ...$data) + { + foreach ($data as $d) { + $this->data[] = $d; + } + } + + public function toString(): string + { + $this->setString(); + return $this->string; + } + + private function setString(): void + { + $this->setArray(); + $this->string = $this->getEncodedString(); + } + + private function setArray(): void + { + if (isset($this->data)) { + $this->array['data'] = $this->getArray($this->data); + } + } + + private function getArray(array $array): array + { + $count = count($array); + switch (true) { + case 1 == $count: + $return = $array[0]->toArray(); + + break; + default: + foreach ($array as $object) { + $return[] = $object->toArray(); + } + break; + } + return $return; + } + + private function setEncodingOptions() + { + $this->encodingOptions = static::DEFAULT_ENCODING_OPTIONS; + if (CLI) { + $this->encodingOptions = $this->encodingOptions | JSON_PRETTY_PRINT; + } + } + + private function getEncodedString(): string + { + try { + return json_encode($this->array, $this->encodingOptions, 512); + } catch (JsonException $e) { + throw new InvalidArgumentException( + (new Message('Unable to encode array as JSON (%m).')) + ->strtr('%m', $e->getMessage()) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/JsonApi/Objects/Error.php b/Chevereto-Chevere/src/JsonApi/Objects/Error.php new file mode 100644 index 000000000..e69de29bb diff --git a/Chevereto-Chevere/src/JsonApi/Objects/JsonApi.php b/Chevereto-Chevere/src/JsonApi/Objects/JsonApi.php new file mode 100644 index 000000000..e69de29bb diff --git a/Chevereto-Chevere/src/JsonApi/Objects/Links.php b/Chevereto-Chevere/src/JsonApi/Objects/Links.php new file mode 100644 index 000000000..e69de29bb diff --git a/Chevereto-Chevere/src/JsonApi/Objects/Meta.php b/Chevereto-Chevere/src/JsonApi/Objects/Meta.php new file mode 100644 index 000000000..e69de29bb diff --git a/Chevereto-Chevere/src/JsonApi/Objects/Resource.php b/Chevereto-Chevere/src/JsonApi/Objects/Resource.php new file mode 100644 index 000000000..e69de29bb diff --git a/Chevereto-Chevere/src/Log.php b/Chevereto-Chevere/src/Log.php new file mode 100644 index 000000000..a2c0063ed --- /dev/null +++ b/Chevereto-Chevere/src/Log.php @@ -0,0 +1,236 @@ +: (imprime 0-4) + * -v: Increaded verbosity (imprime 0-5) + * -vv: (imprime 0-6) + * -vvv: (imprime 0-7). + */ + +/** + * SEVERITY LEVELS. + * + * 0: Emergency + * 1: Alert + * 2: Critical + * 3: Error + * 4: Warning + * 5. Notice + * 6. Informational + * 7: Debug + */ + +/** + * Log provides static global log helpers for the whole app, with CLI support. + * + * Log::debug(,...); + * Log::channel()->error(); + * + * Placeholders: + * %m: Method + * %f: File + * %L: Line + * %F: File:Line + * + * Unable to parse username in app/lib/pico.php:123 + */ +class Log +{ + const SEVERITY_LEVELS = [ + 0 => 'emergency', + 1 => 'alert', + 2 => 'critical', + 3 => 'error', + 4 => 'warning', + 5 => 'notice', + 6 => 'info', + 7 => 'debug', + ]; + const VERBOSITY_MAP = [ + OutputInterface::VERBOSITY_QUIET => [], // -q no messages + OutputInterface::VERBOSITY_NORMAL => [0, 1, 2, 3, 4], // emergency-warning + OutputInterface::VERBOSITY_VERBOSE => [0, 1, 2, 3, 4, 5], // -v emergency-notice + OutputInterface::VERBOSITY_VERY_VERBOSE => [0, 1, 2, 3, 4, 5, 6], // -vv emergency-info + OutputInterface::VERBOSITY_DEBUG => [0, 1, 2, 3, 4, 5, 6, 7], // -vvv emergency-debug + ]; + protected static $loggerContainer = []; + protected static $verboseSet = []; + protected static $useConsole = false; + + /** + * Creates Log object. + */ + protected static function init() + { + if (php_sapi_name() == 'cli') { + static::$useConsole = true; + } + static::$loggerContainer['app'] = new Logger('app'); + $verbosity = static::$useConsole ? Console::cli()->output()->getVerbosity() : OutputInterface::VERBOSITY_NORMAL; + // Set array levelName => int to handle levels + static::$verboseSet = static::VERBOSITY_MAP[$verbosity]; + static::$verboseSet = Utility\Arr::filterArray(static::SEVERITY_LEVELS, static::$verboseSet); + static::$verboseSet = array_flip(static::$verboseSet); + } + + /** + * Allows to set the target log channel. + * (slack, files, email, etc). + */ + // public static function channel(string $channelName) + // { + // } + + protected static function isLevelBeingUsed(string $levelName) + { + return isset(static::$verboseSet[$levelName]); + } + + /** + * Adds a log record at the EMERGENCY level. + * + * Emergency messages indicate that the system is unusable. A panic condition. + * + * TODO: Notify contacts + * + * Examples: + * - "The datacenter is on fire." + * - "The whole building is inside the eye of the tornado." + * + * @param string $message The log message + */ + public static function emergency(string $message) + { + dump($message, 'emergency'); + } + + /** + * Adds a log record at the ALERT level. + * + * Alert messages indicate that action must be taken immediately. + * + * TODO: Notify contacts + * + * Examples: + * - "Unable to connect to DB server." + * - "Class doesn't exists." + * + * @param string $message The log message + */ + public static function alert(string $message) + { + dump($message, 'alert'); + } + + /** + * Adds a log record at the CRITICAL level. + * + * Critical messages indicate conditions that should be corrected immediately, and also indicate failure in a + * secondary system. + * + * Examples: + * - "MySQL server gone." + * - "Unrecoverable DB error: " + * + * @param string $message The log message + */ + public static function critical(string $message) + { + dump($message, 'critical'); + } + + /** + * Adds a log record at the ERROR level. + * + * Error messages indicate non-urgent failures. + * + * Examples: + * - "" + * + * @param string $message The log message + */ + public static function error(string $message) + { + dump($message, 'error'); + } + + /** + * Adds a log record at the WARNING level. + * + * Warning messages indicate that that an error will occur if action is not taken. + * + * Examples: + * - "Filesystem is 90% full." + * - "Method is deprecated in %f:%l. Migrate to to avoid an ALERT." + * + * @param string $message The log message + */ + public static function warning(string $message) + { + dump($message, 'warning'); + } + + /** + * Adds a log record at the NOTICE level. + * + * Notice messages indicate events that are unusual but that are not error conditions. They can be used to spot potential problems, but no immediate action is necessary. + * + * Examples: + * - "Could not load configuration file from . Using defaults." + * - "External storage server is unrecheable. Switching to local storage." + * - "Username already exists. Trying to create user ..." + * + * @param string $message The log message + */ + public static function notice(string $message) + { + // dump('Notice called :', $message); + if (static::isLevelBeingUsed('notice')) { + // dump($message); + Console::cli()->logger()->critical($message); + } + } + + /** + * TODO: Default level? + * Adds a log record at the INFO level. + * + * Informational messages are associated with normal operational behavior. They may be tracked for reporting, measuring throughput, or other purposes, but no action is required. + * + * Examples: + * - "Router initialized." + * - "Showing user profile for user ." + * + * @param string $message The log message + */ + public static function info(string $message) + { + if (static::isLevelBeingUsed(__FUNCTION__)) { + dump($message); + } + } + + /** + * Adds a log record at the DEBUG level. + * + * Debug messages are useful to developers for debugging the application, but are not useful for tracking operations. + * + * Examples: + * - " + * + * @param string $message The log message + */ + public static function debug(string $message) + { + dump($message, 'debug'); + } +} diff --git a/Chevereto-Chevere/src/Message.php b/Chevereto-Chevere/src/Message.php new file mode 100644 index 000000000..2303f540c --- /dev/null +++ b/Chevereto-Chevere/src/Message.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +use JakubOnderka\PhpConsoleColor\ConsoleColor; + +/* + * This class provide a common interface for creating messages. + * + * It works by setting a message string and then using chaineable methods it + * defines a translation string that will be used by __toString(). + * + * Useful for creating messages that needs to wrapped in different tags + * and/or need to be translatable (l10n). + */ + +/** + * @method string code(string $search, string $replace) + * @method string b(string $search, string $replace) + */ +final class Message +{ + /** @var string */ + private $message; + + /** @var array Translation table [search => replace] */ + private $trTable = []; + + /** + * Creates a new Message instance. + * + * @param string $message The message string + */ + public function __construct(string $message) + { + $this->message = $message; + } + + /** + * Magic call method for wrap tags. + * + * @param string $tag Tagname + * @param array $args the arguments, being $args[0] (from) and $args[1] (to) + */ + public function __call(string $tag, array $args): self + { + $search = (string) $args[0]; // $search String to replace for + $replace = (string) $args[1]; // $replace String to replace with + $tagged = $replace != '' ? "<$tag>$replace" : ''; + $this->strtr($search, $tagged); + + return $this; + } + + /** + * Populate the translation table (search => replaces). + * + * @param string $search the value being searched for, otherwise known as the needle + * @param string $replace the replacement value that replaces found search values + */ + public function strtr(string $search, string $replace): self + { + $this->trTable[$search] = $replace; + + return $this; + } + + /** + * Returns the message output using the translation table. + */ + public function toString(): string + { + return strtr($this->message, $this->trTable); + } + + public function toCliString(): string + { + $message = $this->toString(); + return preg_replace_callback('#(.*?)<\/code>#', function ($matches) { + $consoleColor = new ConsoleColor(); + + return $consoleColor->apply(['light_blue'], $matches[1]); + }, $message); + } +} diff --git a/Chevereto-Chevere/src/Path/Path.php b/Chevereto-Chevere/src/Path/Path.php new file mode 100644 index 000000000..52fb82b43 --- /dev/null +++ b/Chevereto-Chevere/src/Path/Path.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Path; + +use const Chevere\ROOT_PATH; + +use Chevere\Message; +use Chevere\Utility\Str; +use RuntimeException; + +final class Path +{ + /** + * Converts relative path to absolute path. + * + * @param string $relativePath a relative path (relative to html root) + * + * @return string absolute path + */ + public static function absolute(string $relativePath): string + { + $relativePath = Str::forwardSlashes($relativePath); + + return ROOT_PATH . $relativePath; + } + + /** + * Converts absolute path to relative path. + * + * @param string $absolutePath an absolute path in the system + * @param string $rootContext root context directory + * + * @return string relative path (relative to html root) + */ + public static function relative(string $absolutePath, string $rootContext = null): ?string + { + $absolutePath = Str::forwardSlashes($absolutePath); + $root = ROOT_PATH; + if ($rootContext) { + $root .= $rootContext . '/'; + } + + return Str::replaceFirst($root, '', $absolutePath); + } + + /** + * Returns whether the file path is an absolute path. + * + * @see https://github.com/symfony/symfony/blob/4.2/src/Symfony/Component/Filesystem/Filesystem.php + * + * @param string $file A file path + * + * @return bool + */ + public static function isAbsolute(string $file): bool + { + return strspn($file, '/\\', 0, 1) + || (\strlen($file) > 3 && ctype_alpha($file[0]) + && ':' === $file[1] + && strspn($file, '/\\', 2, 1)) + || null !== parse_url($file, PHP_URL_SCHEME); + } + + /** + * Creates a path + * + * @return string The created path (absolute) + */ + public static function create(string $path): string + { + if (!mkdir($path, 0777, true)) { + throw new RuntimeException( + (new Message('Unable to create path %path%')) + ->code('%path%', $path) + ); + } + return $path; + } + + /** + * Normalize a filesystem path. + * + * On windows systems, replaces backslashes with forward slashes + * and forces upper-case drive letters. + * Allows for two leading slashes for Windows network shares, but + * ensures that all other duplicate slashes are reduced to a single. + * + * Forked from WordPress. + * + * @param string $path path to normalize + * + * @return string normalized path, without trailing slash + */ + public static function normalize(string $path): string + { + $wrapper = ''; + $stream = static::isStream($path); + if ($stream) { + [$wrapper, $path] = $stream; + $wrapper .= '://'; + } + // Standardise all paths to use / + $path = str_replace('\\', '/', $path ?? ''); + // Replace multiple slashes down to a singular, allowing for network shares having two slashes. + $path = preg_replace('|(?<=.)/+|', '/', $path); + if ($path == null) { + return ''; + } + // Chevereto: Get rid of any extra slashes at the begining if needed + if (Str::startsWith('/', $path)) { + $path = '/' . ltrim($path, '/'); + } + // Windows paths should uppercase the drive letter + if (':' === substr($path, 1, 1)) { + $path = ucfirst($path); + } + + return rtrim($wrapper . $path, '/'); + } + + /** + * Resolve a given path (dots). + * + * Taken from https://stackoverflow.com/a/53598213/1145912 + * + * @param string $path Path to resolve + * + * @return string Resolved path + */ + public static function resolve(string $path): string + { + $n = 0; + $aux = preg_replace("/\/\.\//", '/', $path); + $parts = $aux == null ? [] : explode('/', $aux); + $partsReverse = []; + for ($i = count($parts) - 1; $i >= 0; --$i) { + if (trim($parts[$i]) === '..') { + ++$n; + } else { + if ($n > 0) { + --$n; + } else { + $partsReverse[] = $parts[$i]; + } + } + } + + return implode('/', array_reverse($partsReverse)); + } + + /** + * Test if a given path is a stream URL. + * + * @param string $path the resource path or URL + */ + public static function isStream(string $path): bool + { + if (!Str::contains('://', $path)) { + return false; + } + $explode = explode('://', $path, 2); + + return in_array($explode[0], stream_get_wrappers()); + } + + public static function fromIdentifier(string $identifier): string + { + $that = new PathHandle($identifier); + return $that->path(); + } + + /** + * Adds a trailing slash for a given string. + * + * @param string $dir directory to tail + * + * @return string tailed directory (slash) + */ + public static function tailDir(string $dir): string + { + return Str::rtail($dir, '/'); + } +} diff --git a/Chevereto-Chevere/src/Path/PathHandle.php b/Chevereto-Chevere/src/Path/PathHandle.php new file mode 100644 index 000000000..2ca605dbd --- /dev/null +++ b/Chevereto-Chevere/src/Path/PathHandle.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Path; + +use const Chevere\APP_PATH; + +use InvalidArgumentException; +use Chevere\File; +use Chevere\Message; +use Chevere\Utility\Str; +use LogicException; + +final class PathHandle +{ + /** @var string */ + private $identifier; + + /** @var string */ + private $context = APP_PATH; + + /** @var string absolute path like /home/user/app/ or /home/user/app/file.php */ + private $path; + + /** @var string */ + private $filename; + + /** @var array */ + private $explode; + + /** + * Path identifier refers to the standarized way in which files and paths + * are handled by internal APIs like Hookable or Router. + * + * A path identifier looks like this: + * dirname:file + * + * - The dirname is relative to APP_PATH + * - dirname allows absolute paths + * + * @param string $identifier path identifier relative to app (:) + */ + public function __construct(string $identifier) + { + $this->identifier = $identifier; + $this->validateStringIdentifier(); + $this->validateCharIdentifier(); + $this->validateContext(); + $this->process(); + } + + public function identifier(): string + { + return $this->identifier; + } + + public function path(): string + { + return $this->path; + } + + private function filenameFromIdentifier(): string + { + $this->explode = explode(':', $this->identifier); + $end = end($this->explode); + if (!$end) { + throw new LogicException( + (new Message('The identifier doesn\'t contain a file')) + ->toString() + ); + } + return $end; + } + + private function validateStringIdentifier() + { + if (!($this->identifier != '' && !ctype_space($this->identifier))) { + throw new InvalidArgumentException( + (new Message('String %a needed, %v provided.')) + ->code('%a', '$identifier') + ->code('%v', 'empty or null string') + ->toString() + ); + } + } + + private function validateCharIdentifier() + { + if (Str::contains(':', $this->identifier)) { + if (Str::endsWith(':', $this->identifier)) { + throw new InvalidArgumentException( + (new Message('Wrong string %a format, %v provided (trailing colon).')) + ->code('%a', '$identifier') + ->code('%v', $this->identifier) + ->toString() + ); + } + $this->filename = $this->filenameFromIdentifier(); + if (Str::contains('/', $this->filename)) { + throw new InvalidArgumentException( + (new Message('Wrong string %a format, %v provided (path separators in filename).')) + ->code('%a', '$identifier') + ->code('%v', $this->identifier) + ->toString() + ); + } + } + } + + private function validateContext() + { + if (!Path::isAbsolute($this->context)) { + throw new InvalidArgumentException( + (new Message('String %a must be an absolute path, %v provided.')) + ->code('%a', '$context') + ->code('%v', $this->context) + ->toString() + ); + } + } + + private function process() + { + if (Str::endsWith('.php', $this->identifier) && File::exists($this->identifier)) { + return Path::isAbsolute($this->identifier) ? $this->identifier : Path::absolute($this->identifier); + } + $this->path = Path::normalize($this->identifier); + if (Str::contains(':', $this->path)) { + $this->path = $this->processIdentifier(); + } else { + $this->path = $this->processPath(); + } + // $this->path is not an absolute path neither a wrapper or anything like that + if (!Path::isAbsolute($this->path)) { + $this->path = $this->context.$this->path; + } + // Resolve . and .. + $this->path = Path::resolve($this->path); + } + + private function processIdentifier(): string + { + if (pathinfo($this->filename, PATHINFO_EXTENSION) == null) { + $this->filename .= '.php'; + } + array_pop($this->explode); + $path = join(':', $this->explode); + if (strlen($path) > 0) { + $path = Path::tailDir($path); + } + $path .= $this->filename; + + return $path; + } + + private function processPath(): string + { + // If $this->path does't contains ":", we assume that it is a directory or a explicit filepath + $extension = pathinfo($this->path, PATHINFO_EXTENSION); + // No extension => add trailing slash to path + if ($extension == false) { + return Path::tailDir($this->path); + } + + return $this->path; + } +} diff --git a/Chevereto-Chevere/src/Route/PathValidate.php b/Chevereto-Chevere/src/Route/PathValidate.php new file mode 100644 index 000000000..8ae881c9e --- /dev/null +++ b/Chevereto-Chevere/src/Route/PathValidate.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Route; + +use InvalidArgumentException; +use Chevere\Message; +use Chevere\Utility\Str; +use Chevere\Contracts\Route\PathValidateContract; + +final class PathValidate implements PathValidateContract +{ + /** @var string */ + private $path; + + /** @var bool */ + private $hasHandlebars; + + public function __construct(string $path) + { + $this->setPath($path); + $this->setHasHandlebars(); + if ($this->hasHandlebars) { + $this->validateReservedWildcards(); + } + } + + public function path(): string + { + return $this->path; + } + + public function hasHandlebars(): bool + { + return $this->hasHandlebars; + } + + private function setPath(string $path): void + { + if (!$this->validateFormat($path)) { + throw new InvalidArgumentException( + (new Message("String %s must start with a forward slash, it shouldn't contain neither whitespace, backslashes or extra forward slashes and it should be specified without a trailing slash.")) + ->code('%s', $path) + ->toString() + ); + } + $this->path = $path; + } + + private function validateFormat(string $path): bool + { + if ('/' == $path) { + return true; + } + + return strlen($path) > 0 && Str::startsWith('/', $path) + && $this->validateFormatSlashes($path); + } + + private function validateFormatSlashes(string $path): bool + { + return !Str::endsWith('/', $path) + && !Str::contains('//', $path) + && !Str::contains(' ', $path) + && !Str::contains('\\', $path); + } + + private function validateReservedWildcards(): void + { + if (!(preg_match_all('/{([0-9]+)}/', $this->path) === 0)) { + throw new InvalidArgumentException( + (new Message('Wildcards in the form of %s are reserved.')) + ->code('%s', '/{n}') + ->toString() + ); + } + } + + private function setHasHandlebars(): void + { + $this->hasHandlebars = Str::contains('{', $this->path) || Str::contains('}', $this->path); + } +} diff --git a/Chevereto-Chevere/src/Route/Route.php b/Chevereto-Chevere/src/Route/Route.php new file mode 100644 index 000000000..9b1c2ce80 --- /dev/null +++ b/Chevereto-Chevere/src/Route/Route.php @@ -0,0 +1,281 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Route; + +use LogicException; +use InvalidArgumentException; +use Chevere\Message; +use Chevere\Path\Path; +use Chevere\Controllers\HeadController; +use Chevere\Utility\Str; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Contracts\Http\MethodsContract; +use Chevere\Contracts\Http\MethodContract; +use Chevere\Http\Method; + +// IDEA: L10n support +// FIXME: Use object properties + +final class Route implements RouteContract +{ + /** @const string Route without wildcards. */ + const TYPE_STATIC = 'static'; + + /** @const string Route containing wildcards. */ + const TYPE_DYNAMIC = 'dynamic'; + + /** @const string Regex pattern used by default (no explicit where). */ + const REGEX_WILDCARD_WHERE = '[A-z0-9\_\-\%]+'; + + /** @const string Regex pattern used to detect {wildcard} and {wildcard?}. */ + const REGEX_WILDCARD_SEARCH = '/{([a-z\_][\w_]*\??)}/i'; + + /** @const string Regex pattern used to validate route name. */ + const REGEX_NAME = '/^[\w\-\.]+$/i'; + + /** @var string Route id relative to the ArrayFile */ + private $id; + + /** @var string Route path like /api/users/{user} */ + private $path; + + /** @var string Route name (if any, must be unique) */ + private $name; + + /** @var array Where clauses based on wildcards */ + private $wheres; + + /** @var array ['method' => 'controller',] */ + private $methods; + + /** @var array [MiddlewareContract,] */ + private $middlewares; + + /** @var array */ + private $wildcards; + + /** @var string Route path representation with placeholder wildcards like /api/users/{0} */ + private $key; + + /** @var array Contains all the possible $set combinations when using optional wildcards */ + private $keyPowerSet; + + /** @var array An array containg details about the Route maker */ + private $maker; + + /** @var string */ + private $regex; + + /** @var string */ + private $type; + + public function __construct(string $path, string $controller = null) + { + $pathValidate = new PathValidate($path); + $this->path = $path; + $this->maker = $this->getMakerData(); + if ($pathValidate->hasHandlebars()) { + $set = new Set($this->path); + $this->key = $set->key(); + $this->keyPowerSet = $set->keyPowerSet(); + $this->wildcards = $set->toArray(); + } else { + $this->key = $this->path; + } + $this->handleType(); + if (isset($controller)) { + $this->setMethod(new Method('GET', $controller)); + } + } + + public function id(): string + { + return $this->id; + } + + public function path(): string + { + return $this->path; + } + + public function name(): string + { + return $this->name; + } + + public function hasName(): bool + { + return isset($this->name); + } + + public function wheres(): array + { + return $this->wheres ?? []; + } + + public function middlewares(): array + { + return $this->middlewares ?? []; + } + + public function wildcardName(int $key): string + { + return $this->wildcards[$key] ?? ''; + } + + public function keyPowerSet(): array + { + return $this->keyPowerSet ?? []; + } + + public function type(): string + { + return $this->type; + } + + public function regex(): string + { + return $this->regex; + } + + public function setName(string $name): RouteContract + { + // Validate $name + if (!preg_match(static::REGEX_NAME, $name)) { + throw new InvalidArgumentException( + (new Message("Expecting at least one alphanumeric, underscore, hypen or dot character. String '%s' provided.")) + ->code('%s', $name) + ->code('%p', static::REGEX_NAME) + ->toString() + ); + } + $this->name = $name; + + return $this; + } + + public function setWhere(string $wildcardName, string $regex): RouteContract + { + $wildcard = new Wildcard($wildcardName, $regex); + $wildcard->bind($this); + $this->wheres[$wildcardName] = $regex; + + return $this; + } + + public function setMethod(MethodContract $method): RouteContract + { + if (isset($this->methods[$method->method()])) { + throw new InvalidArgumentException( + (new Message('Method %s has been already registered.')) + ->code('%s', $method->method())->toString() + ); + } + $this->methods[$method->method()] = $method->controller(); + + return $this; + } + + public function setMethods(MethodsContract $methods): RouteContract + { + foreach ($methods as $method) { + $this->setMethod($method); + } + + return $this; + } + + public function setId(string $id): RouteContract + { + $this->id = $id; + + return $this; + } + + public function addMiddleware(string $callable): RouteContract + { + // $this->middlewares[] = $this->getCallableSome($callable); + $this->middlewares[] = $callable; + + return $this; + } + + public function getController(string $httpMethod): string + { + $controller = $this->methods[$httpMethod]; + if (!isset($controller)) { + throw new LogicException( + (new Message('No controller is associated to HTTP method %s.')) + ->code('%s', $httpMethod) + ->toString() + ); + } + + return $controller; + } + + public function fill(): RouteContract + { + if (isset($this->wildcards)) { + foreach ($this->wildcards as $k => $v) { + if (!isset($this->wheres[$v])) { + $this->wheres[$v] = static::REGEX_WILDCARD_WHERE; + } + } + } + if (isset($this->methods['GET']) && !isset($this->methods['HEAD'])) { + $this->setMethod(new Method('HEAD', HeadController::class)); + } + $this->regex = $this->getRegex($this->key ?? $this->path); + + return $this; + } + + public function getRegex(string $pattern): string + { + $regex = '^' . $pattern . '$'; + if (!Str::contains('{', $regex)) { + return $regex; + } + if (isset($this->wildcards)) { + foreach ($this->wildcards as $k => $v) { + $regex = str_replace("{{$k}}", '(' . $this->wheres[$v] . ')', $regex); + } + } + + return $regex; + } + + private function getMakerData(): array + { + $maker = debug_backtrace(0, 3)[2]; + $maker['file'] = Path::relative($maker['file']); + + return $maker; + } + + private function handleType(): void + { + if (!isset($this->key)) { + $this->type = Route::TYPE_STATIC; + } else { + // Sets (optionals) are like /route/{0} + $pregReplace = preg_replace('/{[0-9]+}/', '', $this->key); + if (null != $pregReplace) { + $pregReplace = trim(Path::normalize($pregReplace), '/'); + } + $this->type = isset($pregReplace) ? Route::TYPE_DYNAMIC : Route::TYPE_STATIC; + } + } +} diff --git a/Chevereto-Chevere/src/Route/Set.php b/Chevereto-Chevere/src/Route/Set.php new file mode 100644 index 000000000..61bbcad3f --- /dev/null +++ b/Chevereto-Chevere/src/Route/Set.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Route; + +use LogicException; +use Chevere\Message; +use Chevere\Path\Path; +use Chevere\Utility\Str; +use Chevere\Utility\Arr; + +final class Set +{ + /** @var string The route path */ + private $path; + + /** @var string Path key set representation ({wildcards} replaced by {n}) */ + private $key; + + /** @var array */ + private $matches; + + /** @var array string[] */ + private $wildcards; + + /** @var array All the key sets for the route (optionals combo) */ + private $keyPowerSet; + + /** @var array Optional wildcards */ + private $optionals; + + /** @var array Optional wildcards index */ + private $optionalsIndex; + + /** @var array Mandatory wildcards index */ + private $mandatoryIndex; + + public function __construct(string $path) + { + $this->path = $path; + // $matches[0] => [{wildcard}, {wildcard?},...] + // $matches[1] => [wildcard, wildcard?,...] + if (!preg_match_all(Route::REGEX_WILDCARD_SEARCH, $this->path, $matches)) { + return; + } + $this->matches = $matches; + $this->key = $path; + $this->optionals = []; + $this->optionalsIndex = []; + $this->handleMatches(); + $this->handleOptionals(); + } + + public function key(): string + { + return $this->key; + } + + public function matches(): array + { + return $this->matches ?? []; + } + + public function toArray(): array + { + return $this->wildcards ?? []; + } + + public function keyPowerSet(): array + { + return $this->keyPowerSet ?? []; + } + + private function handleMatches(): void + { + foreach ($this->matches[0] as $k => $v) { + // Change {wildcard} to {n} (n is the wildcard index) + if (isset($this->key)) { + $this->key = Str::replaceFirst($v, "{{$k}}", $this->key); + } + $wildcard = $this->matches[1][$k]; + if (Str::endsWith('?', $wildcard)) { + $wildcardTrim = Str::replaceLast('?', '', $wildcard); + $this->optionals[] = $k; + $this->optionalsIndex[$k] = $wildcardTrim; + } else { + $wildcardTrim = $wildcard; + } + if (in_array($wildcardTrim, $this->wildcards ?? [])) { + throw new LogicException( + (new Message('Must declare one unique wildcard per capturing group, duplicated %s detected in route %r.')) + ->code('%s', $this->matches[0][$k]) + ->code('%r', $this->path) + ->toString() + ); + } + $this->wildcards[] = $wildcardTrim; + } + } + + private function handleOptionals(): void + { + if (!empty($this->optionals)) { + $mandatoryDiff = array_diff($this->wildcards ?? [], $this->optionalsIndex); + $this->mandatoryIndex = $this->getIndex($mandatoryDiff); + // Generate the optionals power set, keeping its index keys in case of duplicated optionals + $powerSet = Arr::powerSet($this->optionals, true); + // Build the route set, it will contain all the possible route combinations + $this->keyPowerSet = $this->processPowerSet($powerSet); + } + } + + private function getIndex(array $diff): array + { + $index = []; + foreach ($diff as $k => $v) { + $index[$k] = null; + } + + return $index; + } + + private function processPowerSet(array $powerSet): array + { + $routeSet = []; + foreach ($powerSet as $set) { + $auxSet = $this->key; + $auxWildcards = $this->mandatoryIndex; + foreach ($set as $replaceKey => $replaceValue) { + $search = $this->optionals[$replaceKey]; + if ($replaceValue !== null) { + $replaceValue = "{{$replaceValue}}"; + $auxWildcards[$search] = null; + } + $auxSet = str_replace("{{$search}}", $replaceValue ?? '', $auxSet); + $auxSet = Path::normalize($auxSet); + } + ksort($auxWildcards); + /* + * Maps expected regex indexed matches [0,1,2,] to registered wildcard index [index=>n]. + * For example, a set /test-{0}--{2} will capture 0->0 and 1->2. Storing the expected index allows\ + * to easily map matches => wildcards => values. + */ + $routeSet[$auxSet] = array_keys($auxWildcards); + } + + return $routeSet; + } +} diff --git a/Chevereto-Chevere/src/Route/Wildcard.php b/Chevereto-Chevere/src/Route/Wildcard.php new file mode 100644 index 000000000..8e33132d9 --- /dev/null +++ b/Chevereto-Chevere/src/Route/Wildcard.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Route; + +use LogicException; +use InvalidArgumentException; +use Chevere\Message; +use Chevere\Validate; +use Chevere\Utility\Str; + +final class Wildcard +{ + /** @var string */ + private $wildcardName; + + /** @var string */ + private $wildcardString; + + /** @var string */ + private $regex; + + /** @var Route */ + private $route; + + public function __construct(string $wildcardName, string $regex) + { + $this->wildcardName = $wildcardName; + $this->wildcardString = "{{$wildcardName}}"; + $this->regex = $regex; + $this->validateFormat(); + $this->validateRegex(); + } + + public function bind(Route $route) + { + $this->route = $route; + $this->validateRoutePathMatch(); + $this->validateRouteUniqueWildcard(); + } + + private function validateFormat(): void + { + if (!(!Str::startsWithNumeric($this->wildcardName) && preg_match('/^[a-z0-9_]+$/i', $this->wildcardName))) { + throw new InvalidArgumentException( + (new Message("String %s must contain only alphanumeric and underscore characters and it shouldn't start with a numeric value.")) + ->code('%s', $this->wildcardName) + ->toString() + ); + } + } + + private function validateRegex() + { + if (!Validate::regex('/'.$this->regex.'/')) { + throw new InvalidArgumentException( + (new Message('Invalid regex pattern %regex%.')) + ->code('%regex%', $this->regex) + ->toString() + ); + } + } + + private function validateRoutePathMatch(): void + { + if (!(Str::contains("{{$this->wildcardName}}", $this->route->path()) || Str::contains('{'."$this->wildcardName?".'}', $this->route->path()))) { + throw new LogicException( + (new Message("Wildcard %wildcard% doesn't exists in %path%.")) + ->code('%wildcard%', $this->wildcardString) + ->code('%path%', $this->route->path()) + ->toString() + ); + } + } + + private function validateRouteUniqueWildcard(): void + { + if (isset($this->route->wheres()[$this->wildcardName])) { + throw new LogicException( + (new Message('Where clause for %s wildcard has been already declared.')) + ->code('%s', $this->wildcardString) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Router/Exception/RouteNotFoundException.php b/Chevereto-Chevere/src/Router/Exception/RouteNotFoundException.php new file mode 100644 index 000000000..29ec692f5 --- /dev/null +++ b/Chevereto-Chevere/src/Router/Exception/RouteNotFoundException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Router\Exception; + +use Exception; + +class RouteNotFoundException extends Exception +{ } diff --git a/Chevereto-Chevere/src/Router/Maker.php b/Chevereto-Chevere/src/Router/Maker.php new file mode 100644 index 000000000..705abe56a --- /dev/null +++ b/Chevereto-Chevere/src/Router/Maker.php @@ -0,0 +1,194 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Router; + +use Chevere\ArrayFile\ArrayFile; +use Chevere\ArrayFile\ArrayFileCallback; +use LogicException; +use Chevere\Message; +use Chevere\Route\Route; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Path\PathHandle; +use Chevere\Cache\Cache; +use Chevere\Type; + +/** + * Maker takes a bunch of Routes and generates a routing table (php array). + */ +final class Maker +{ + // const ID = 'id'; + // const SET = 'set'; + + const REGEX_TEPLATE = '#^(?%s)$#x'; + + /** @var string Regex representation, used when resolving routing */ + private $regex; + + /** @var array Route members (objects, serialized) [id => Route] */ + private $routes; + + /** @var array Contains ['/path' => [id, 'route/key']] */ + private $routesIndex; + + /** @var array An array containing the named routes [name => [id, fileHandle]] */ + private $named; + + /** @var array [basename => [route id,]]. */ + private $baseIndex; + + /** @var array [regex => Route id]. */ + private $regexIndex; + + /** @var array Static routes */ + private $statics; + + /** @var RouteContract */ + private $route; + + /** @var array Stores the map for a given route ['group' => group, 'id' => routeId] */ + private $routeMap; + + /** @var Cache */ + private $cache; + + public function __construct() + { } + + /** + * {@inheritdoc} + */ + public function addRoute(RouteContract $route, string $group): void + { + $this->route = $route->fill(); + $this->routeMap = ['group' => $group, 'id' => $this->route->id()]; + $this->validateUniqueRoutePath(); + $this->handleRouteName(); + $this->routes[] = $route; + $id = array_key_last($this->routes); + $this->baseIndex[$group][] = array_key_last($this->routes); + $keyPowerSet = $route->keyPowerSet(); + if (!empty($keyPowerSet)) { + $ix = $id; + foreach ($keyPowerSet as $set => $index) { + ++$ix; + $this->routes[] = [$id, (string) $set]; + $this->regexIndex[$route->getRegex((string) $set)] = $ix; + } + } else { + // n => .. => regex => route + $this->regexIndex[$route->regex()] = $id; + if (Route::TYPE_STATIC == $route->type()) { + $this->statics[$route->path()] = $id; + } + } + + $this->regex = $this->getRegex(); + $this->routesIndex[$this->route->path()] = $this->routeMap; + } + + /** + * Adds routes (ArrayFile) specified by path handle. + * + * @param array $paramRoutes ['routes:web', 'routes:dashboard'] + */ + public function addRoutesArrays(array $paramRoutes): void + { + foreach ($paramRoutes as $fileHandleString) { + $arrayFile = new ArrayFile( + new PathHandle($fileHandleString), + new Type(RouteContract::class) + ); + $arrayFileWrap = new ArrayFileCallback($arrayFile, function ($k, $route) { + $route->setId((string) $k); + }); + foreach ($arrayFileWrap as $route) { + $this->addRoute($route, $fileHandleString); + } + } + } + + public function regex(): string + { + return $this->regex; + } + + public function routes(): array + { + return $this->routes; + } + + public function routesIndex(): array + { + return $this->routesIndex; + } + + public function setCache() + { + $this->cache = new Cache('router'); + $this->cache->put('regex', $this->regex); + $this->cache->put('routes', $this->routes); + $this->cache->put('routesIndex', $this->routesIndex); + } + + public function cache(): Cache + { + return $this->cache; + } + + /** + * {@inheritdoc} + */ + private function getRegex(): string + { + $regex = []; + foreach ($this->regexIndex as $k => $v) { + preg_match('#\^(.*)\$#', $k, $matches); + $regex[] = '|' . $matches[1] . " (*:$v)"; + } + + return sprintf(static::REGEX_TEPLATE, implode('', $regex)); + } + + private function validateUniqueRoutePath(): void + { + $keyedRoute = $this->routesIndex[$this->route->path()] ?? null; + if (isset($keyedRoute)) { + throw new LogicException( + (new Message('Route key %s has been already declared by %r.')) + ->code('%s', $this->route->path()) + ->code('%r', $keyedRoute['id'] . '@' . $keyedRoute['group']) + ->toString() + ); + } + } + + private function handleRouteName(): void + { + $name = $this->route->hasName() ? $this->route->name() : null; + if (!isset($name)) { + return; + } + $namedRoute = $this->named[$name] ?? null; + if (isset($namedRoute)) { + throw new LogicException( + (new Message('Route name %s has been already taken by %r.')) + ->code('%s', $name) + ->code('%r', $namedRoute[0] . '@' . $namedRoute[1]) + ->toString() + ); + } + $this->named[$name] = $this->routeMap; + } +} diff --git a/Chevereto-Chevere/src/Router/Resolver.php b/Chevereto-Chevere/src/Router/Resolver.php new file mode 100644 index 000000000..956041147 --- /dev/null +++ b/Chevereto-Chevere/src/Router/Resolver.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Router; + +use LogicException; +use Chevere\Message; +use Chevere\Route\Route; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Contracts\Router\ResolverContract; +use Throwable; + +final class Resolver +{ + /** @var RouteContract */ + private $route; + + public function __construct(string $serialized) + { + try { + $this->route = unserialize($serialized, ['allowed_classes' => [Route::class]]); + } catch (Throwable $e) { + throw new LogicException( + (new Message('Unable to unserialize: %e')) + ->code('%e', $e->getMessage()) + ->toString() + ); + } + } + + public function get(): RouteContract + { + return $this->route; + } +} diff --git a/Chevereto-Chevere/src/Router/Router.php b/Chevereto-Chevere/src/Router/Router.php new file mode 100644 index 000000000..09d3307bb --- /dev/null +++ b/Chevereto-Chevere/src/Router/Router.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Router; + +use Chevere\Message; +use Chevere\Cache\Cache; +use Chevere\Contracts\Route\RouteContract; +use Chevere\Contracts\Router\RouterContract; +use Chevere\Router\Exception\RouteNotFoundException; + +/**s + * Routes takes a bunch of Routes and generates a routing table (php array). + */ +final class Router implements RouterContract +{ + const REGEX_TEPLATE = '#^(?%s)$#x'; + + /** @var string Regex representation, used when resolving routing */ + private $regex; + + /** @var array Route members (objects, serialized) [id => Route] */ + private $routes; + + /** @var array Contains ['/path' => [id, 'route/key']] */ + private $routesIndex; + + /** @var array Arguments taken from wildcard matches */ + private $arguments; + + public function __construct(Maker $maker = null) + { + if (isset($maker)) { + $this->regex = $maker->regex(); + $this->routes = $maker->routes(); + $this->routesIndex = $maker->routesIndex(); + $maker->setcache(); + } else { + $cache = new Cache('router'); + $this->regex = $cache->get('regex')->raw(); + $this->routes = $cache->get('routes')->raw(); + $this->routesIndex = $cache->get('routesIndex')->raw(); + } + } + + public function arguments(): array + { + return $this->arguments ?? []; + } + + /** + * {@inheritdoc} + */ + public function resolve(string $pathInfo): RouteContract + { + if (preg_match($this->regex, $pathInfo, $matches)) { + return $this->resolver($matches); + } + throw new RouteNotFoundException( + (new Message('No route defined for %s')) + ->code('%s', $pathInfo) + ->toString() + ); + } + + private function resolver(array $matches): RouteContract + { + $id = $matches['MARK']; + unset($matches['MARK']); + array_shift($matches); + $route = $this->routes[$id]; + // Array when the route is a powerSet [id, set] + if (is_array($route)) { + $set = $route[1]; + $route = $this->routes[$route[0]]; + } + if (is_string($route)) { + $resolver = new Resolver($route); + $route = $resolver->get(); + $this->routes[$id] = $route; + } + $this->arguments = []; + if (isset($set)) { + foreach ($matches as $k => $v) { + $wildcardId = $route->keyPowerSet()[$set][$k]; + $wildcardName = $route->wildcardName($wildcardId); + $this->arguments[$wildcardName] = $v; + } + } + + return $route; + } +} diff --git a/Chevereto-Chevere/src/Runtime/Runtime.php b/Chevereto-Chevere/src/Runtime/Runtime.php new file mode 100644 index 000000000..a5c1b0d28 --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Runtime.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime; + +use Chevere\Data\Data; +use Chevere\Contracts\Runtime\RuntimeSetContract; +use Chevere\Contracts\DataContract; +use Chevere\Data\Traits\DataKeyTrait; +use ReflectionClass; + +/** + * Runtime applies runtime config and provide data about the App Runtime. + */ +final class Runtime +{ + use DataKeyTrait; + + /** @var DataContract */ + private $data; + + public function __construct(RuntimeSetContract ...$runtimeContract) + { + $this->data = new Data(); + foreach ($runtimeContract as $k => $runtimeSet) { + $this->data->setKey($runtimeSet->name(), $runtimeSet->value()); + } + $this->data->setKey('errorReportingLevel', error_reporting()); + // $this->config = $this->data->toArray(); + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDebug.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDebug.php new file mode 100644 index 000000000..23f5ffbcf --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDebug.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use RuntimeException; +use Chevere\Message; +use Chevere\Contracts\Runtime\RuntimeSetContract; +use Chevere\Runtime\Traits\RuntimeSet; + +class RuntimeSetDebug implements RuntimeSetContract +{ + use RuntimeSet; + + const ACCEPT = [0, 1]; + + public function set(): void + { + if (!in_array($this->value, static::ACCEPT)) { + throw new RuntimeException( + (new Message('Expecting %expecting%, %value% provided.')) + ->code('%expecting%', implode(', ', static::ACCEPT)) + ->code('%value%', $this->value) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDefaultCharset.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDefaultCharset.php new file mode 100644 index 000000000..e74b9d290 --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetDefaultCharset.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use RuntimeException; +use Chevere\Message; +use Chevere\Runtime\Traits\RuntimeSet; +use Chevere\Contracts\Runtime\RuntimeSetContract; + +class RuntimeSetDefaultCharset implements RuntimeSetContract +{ + use RuntimeSet; + + public function set(): void + { + if (!@ini_set('default_charset', $this->value)) { + throw new RuntimeException( + (new Message('Unable to set %s %v.')) + ->code('%s', 'default_charset') + ->code('%v', $this->value) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetErrorHandler.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetErrorHandler.php new file mode 100644 index 000000000..0f28d986b --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetErrorHandler.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use InvalidArgumentException; +use Chevere\Message; +use Chevere\Contracts\Runtime\RuntimeSetContract; +use Chevere\Runtime\Traits\RuntimeSet; + +class RuntimeSetErrorHandler implements RuntimeSetContract +{ + use RuntimeSet; + + public function set(): void + { + if (null == $this->value) { + $this->restoreErrorHandler(); + } else { + if (!is_callable($this->value)) { + throw new InvalidArgumentException( + (new Message('Runtime value must be a valid callable for %subject%')) + ->code('%subject%', 'set_error_handler') + ); + } + set_error_handler($this->value); + } + } + + private function restoreErrorHandler(): void + { + restore_error_handler(); + $this->value = (string) set_error_handler(function () { }); + restore_error_handler(); + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetExceptionHandler.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetExceptionHandler.php new file mode 100644 index 000000000..01e406d4f --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetExceptionHandler.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use InvalidArgumentException; +use Chevere\Message; +use Chevere\Runtime\Traits\RuntimeSet; +use Chevere\Contracts\Runtime\RuntimeSetContract; + +class RuntimeSetExceptionHandler implements RuntimeSetContract +{ + use RuntimeSet; + + public function set(): void + { + if (null == $this->value) { + $this->restoreExceptionHandler(); + } else { + if (!is_callable($this->value)) { + throw new InvalidArgumentException( + (new Message('Runtime value must be a valid callable for %subject%')) + ->code('%subject%', 'set_exception_handler') + ); + } + set_exception_handler($this->value); + } + } + + private function restoreExceptionHandler(): void + { + restore_exception_handler(); + $this->value = (string) set_exception_handler(function () { }); + restore_exception_handler(); + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetLocale.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetLocale.php new file mode 100644 index 000000000..2312b50a2 --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetLocale.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use RuntimeException; +use Chevere\Message; +use Chevere\Contracts\Runtime\RuntimeSetContract; +use Chevere\Runtime\Traits\RuntimeSet; + +class RuntimeSetLocale implements RuntimeSetContract +{ + use RuntimeSet; + + public function set(): void + { + if (!setlocale(LC_ALL, $this->value)) { + throw new RuntimeException( + (new Message('The locale functionality is not implemented on your platform, the specified locale %locale% does not exist or the category name %category% is invalid.')) + ->code('%category%', 'LC_ALL') + ->code('%locale%', $this->value) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetPrecision.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetPrecision.php new file mode 100644 index 000000000..a104b4baa --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetPrecision.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use Chevere\Contracts\Runtime\RuntimeSetContract; +use RuntimeException; +use Chevere\Message; +use Chevere\Runtime\Traits\RuntimeSet; + +class RuntimeSetPrecision implements RuntimeSetContract +{ + use RuntimeSet; + + public function set(): void + { + if (!@ini_set('precision', $this->value)) { + throw new RuntimeException( + (new Message('Unable to set %s %v.')) + ->code('%s', 'default_charset') + ->code('%v', $this->value) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetTimeZone.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetTimeZone.php new file mode 100644 index 000000000..2ee91bc1b --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetTimeZone.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use InvalidArgumentException; +use RuntimeException; +use Chevere\Message; +use Chevere\Contracts\Runtime\RuntimeSetContract; +use Chevere\Runtime\Traits\RuntimeSet; +use Chevere\Validate; + +class RuntimeSetTimeZone implements RuntimeSetContract +{ + use RuntimeSet; + + public function set(): void + { + if (date_default_timezone_get() == $this->value) { + return; + } + if ('UTC' != $this->value && !Validate::timezone($this->value)) { + throw new InvalidArgumentException( + (new Message('Invalid timezone %timezone%.')) + ->code('%timezone%', $this->value) + ->toString() + ); + } + if (!@date_default_timezone_set($this->value)) { + throw new RuntimeException( + (new Message('False return on %s(%v).')) + ->code('%s', 'date_default_timezone_set') + ->code('%v', $this->value) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetUriScheme.php b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetUriScheme.php new file mode 100644 index 000000000..2804d313c --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Sets/RuntimeSetUriScheme.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Sets; + +use RuntimeException; +use Chevere\Message; +use Chevere\Contracts\Runtime\RuntimeSetContract; +use Chevere\Runtime\Traits\RuntimeSet; + +class RuntimeSetUriScheme implements RuntimeSetContract +{ + use RuntimeSet; + + public function set(): void + { + $accept = ['http', 'https']; + if (!in_array($this->value, $accept)) { + throw new RuntimeException( + (new Message('Expecting %expecting%, %value% provided.')) + ->code('%expecting%', implode(', ', $accept)) + ->code('%value%', $this->value) + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Runtime/Traits/RuntimeSet.php b/Chevereto-Chevere/src/Runtime/Traits/RuntimeSet.php new file mode 100644 index 000000000..93edcd893 --- /dev/null +++ b/Chevereto-Chevere/src/Runtime/Traits/RuntimeSet.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Runtime\Traits; + +use Chevere\Utility\Str; +use Chevere\Contracts\DataContract; + +trait RuntimeSet +{ + /** @var string */ + private $value; + + /** @var DataContract */ + private $data; + + public function __construct(string $value = null) + { + $this->value = $value; + $this->set(); + } + + public function value(): ?string + { + return $this->value; + } + + public function name(): string + { + $explode = explode('\\', __CLASS__); + $name = Str::replaceFirst('RuntimeSet', '', end($explode)); + return lcfirst($name); + } + + abstract public function set(): void; +} diff --git a/Chevereto-Chevere/src/Stopwatch.php b/Chevereto-Chevere/src/Stopwatch.php new file mode 100644 index 000000000..5f12f27c0 --- /dev/null +++ b/Chevereto-Chevere/src/Stopwatch.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +use InvalidArgumentException; + +/** + * A simple stopwatch, useful for userland execution time measurement. + */ +final class Stopwatch +{ + /** @var array [flag => $timeElapsedRead relative to previous record ] */ + private $records; + + /** @var float */ + private $timeStart; + + /** @var float */ + private $timeEnd; + + /** @var float Microtime */ + private $timeElapsed; + + /** @var string The time elapsed, in miliseconds with tis unit (100 ms) */ + private $timeElapsedRead; + + public function __construct() + { + $this->records = [ + 'start' => 0, + ]; + $this->timeStart = microtime(true); + } + + public function record(string $flagName): void + { + $now = microtime(true); + if ('stop' == $flagName) { + throw new InvalidArgumentException( + (new Message('Use of reserved flag name %flagName%.')) + ->code('%flagName%', 'stop') + ->toString() + ); + } + if (isset($this->records[$flagName])) { + throw new InvalidArgumentException( + (new Message('Flag name %flagName% has be already registered, you must use an unique flag for each %className% instance.')) + ->code('%flagName%', $flagName) + ->code('%className%', __CLASS__) + ->toString() + ); + } + $then = microtime(true); + $this->records[$flagName] = $then - ($now - $then); + } + + // $this->microtimeToRead() + + public function stop(): void + { + $this->timeEnd = microtime(true); + $this->timeElapsed = $this->timeEnd - $this->timeStart; + $this->timeElapsedRead = $this->microtimeToRead($this->timeElapsed); + $prevMicrotime = 0; + $this->records['stop'] = $this->timeEnd; + foreach ($this->records as $flag => $microtime) { + $this->records[$flag] = $this->microtimeToRead($microtime - $prevMicrotime); + $prevMicrotime = $microtime > 0 ? $microtime : $this->timeStart; + } + } + + public function records(): array + { + return $this->records; + } + + public function timeElapsed(): float + { + return $this->timeElapsed; + } + + public function timeElapsedRead(): string + { + return $this->timeElapsedRead; + } + + private function microtimeToRead(float $microtime): string + { + return number_format($microtime * 1000, 2) . ' ms'; + } +} diff --git a/Chevereto-Chevere/src/Traits/CallableTrait.php b/Chevereto-Chevere/src/Traits/CallableTrait.php new file mode 100644 index 000000000..9648c2ccc --- /dev/null +++ b/Chevereto-Chevere/src/Traits/CallableTrait.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Traits; + +use LogicException; +use Chevere\File; +use Chevere\Path\Path; +use Chevere\Message; + +trait CallableTrait +{ + /** + * Retuns the callable some (callable string, callable relative filepath). + * + * @param string $callableString a callable string + */ + public function getCallableSome(string $callableString): ?string + { + if (is_callable($callableString)) { + return $callableString; + } else { + if (class_exists($callableString)) { + if (method_exists($callableString, '__invoke')) { + return (string) $callableString; + } else { + throw new LogicException( + (new Message('Missing %s method in class %c')) + ->code('%s', '__invoke') + ->code('%c', $callableString) + ->toString() + ); + } + } else { + $callableFile = Path::fromIdentifier($callableString); + $this->checkCallableFile($callableFile); + + return Path::relative($callableFile); + } + } + } + + /** + * Checks if a callable file exists. + */ + protected function checkCallableFile(string $callableFile) + { + // Check callable existance + if (!File::exists($callableFile, true)) { + throw new LogicException( + (new Message("Callable %s doesn't exists.")) + ->code('%s', $callableFile) + ->toString() + ); + } + // Had to make this sandwich since we are calling an anon callable. + $errorLevel = error_reporting(); + error_reporting($errorLevel ^ E_NOTICE); + $anonCallable = include $callableFile; + error_reporting($errorLevel); + // Check callable + if (!is_callable($anonCallable)) { + throw new LogicException( + (new Message('File %f is not a valid %t.')) + ->code('%f', $callableFile) + ->code('%t', 'callable') + ->toString() + ); + } + } +} diff --git a/Chevereto-Chevere/src/Traits/HookableTrait.php b/Chevereto-Chevere/src/Traits/HookableTrait.php new file mode 100644 index 000000000..af6a60019 --- /dev/null +++ b/Chevereto-Chevere/src/Traits/HookableTrait.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Traits; + +use Chevere\Hooking\Hook; + +/** + * This class provides a hookable API allowing to define anchor points where + * external code can be added on execution. + * + * For anchors defined in object context, the object itself will be passed + * throught the hookable, making possible to execute external code that + * will interact directly with the object. + * + * Your hookable entries can be defined to allow code injection before and or + * after your hookable code. + * + * Any of your classes can provide Hookeable functionality by simply extending + * this class. + * + * Pretend you have the following section inside your code: + * + * $this->prop = 'value'; + * $this->process($this->prop); + * + * That code cast process($this->prop). If you want to allow external code + * there, simly wrap the above code in something like this: + * + * $value = 'value'; + * $this->hookable('anchor', function($that) use ($value) { + * $that->prop = $value; + * }); + * $this->process($this->prop); + * + * In the above example, hooks can be registered before and after. Hooks before + * will be executed before $that->prop = $value; $that is $this. + * + * A hook should be defined like this: + * + * Hookable::after('anchor@relativePath:basename', function($that) { + * $that->prop = filter($that->prop); + * }); + * + * The object is passed directly, so the methods and properties will be + * accessible based on class visibility scope. + * + * @see Controller + * @see SimpleController + * @see Router + */ +trait HookableTrait +{ + /** + * Register and run hookable code entries before and after. + * + * Hook::before('anchor@relativePath:basename', function($that) { + * $that + * }); + * + * @param string $anchor hook anchor + * @param callable $callable callable + */ + public function hookable(string $anchor, callable $callable): void + { + Hook::exec($anchor, $callable, $this); + } + + /** + * Register a hookable entry before. + * + * @see hookable() + */ + public function hookableBefore(string $anchor, callable $callable): void + { + Hook::execBefore($anchor, $callable, $this); + } + + /** + * Register a hookable entry after. + * + * @see hookable() + */ + public function hookableAfter(string $anchor, callable $callable): void + { + Hook::execAfter($anchor, $callable, $this); + } + + /* + * Static version of hookable() + * + * Static versions are limited as $this is not being passed through. + * No variable can be touched, it just adds procedures. + * + * @see hookable() + */ + // public static function section(string $anchor, callable $callable) : void + // { + // Hook::exec(...func_get_args()); + // } + /* + * Static version of hookableBefore + * + * @see hookableBefore() + */ + // public static function before(string $anchor, callable $callable) : void + // { + // Hook::execBefore(...func_get_args()); + // } + /* + * Static version hookable after + * + * @see hookableAfter() + */ + // public static function after(string $anchor, callable $callable) : void + // { + // Hook::execAfter(...func_get_args()); + // } +} diff --git a/Chevereto-Chevere/src/Traits/PrintableTrait.php b/Chevereto-Chevere/src/Traits/PrintableTrait.php new file mode 100644 index 000000000..16423e70a --- /dev/null +++ b/Chevereto-Chevere/src/Traits/PrintableTrait.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Traits; + +/** + * Printable provides an interface for classes who may use __toString(). + * + * @see Interfaces\PrintableInterface + * @see Utility\Benchmark + * @see JSON + */ +trait PrintableTrait +{ + /** + * The printable string. + */ + protected $printable; + + /** + * Allows to cast this object as string. + * + * @return string printable + */ + public function __toString(): ?string + { + if ($this->printable == null) { + $this->exec(); + } + + return $this->printable ?? ''; + } + + /** + * Print object string. + */ + public function print() + { + if ($this->printable == null) { + $this->exec(); + } + echo (string) $this; // invokes __toString, such trucazo. + } + + abstract public function exec(): void; +} diff --git a/Chevereto-Chevere/src/Type.php b/Chevereto-Chevere/src/Type.php new file mode 100644 index 000000000..a7962fb93 --- /dev/null +++ b/Chevereto-Chevere/src/Type.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +/** + * Type provides type hinting and validation toolchain. + */ +final class Type +{ + /** @const array Type validators [primitive => validator], taken from https://www.php.net/manual/en/ref.var.php */ + const TYPE_VALIDATORS = [ + 'array' => 'is_array', + 'bool' => 'is_bool', + 'callable' => 'is_callable', + 'countable' => 'is_countable', + 'double' => 'is_double', + 'float' => 'is_float', + 'int' => 'is_int', + 'integer' => 'is_integer', + 'iterable' => 'is_iterable', + 'long' => 'is_long', + 'null' => 'is_null', + 'numeric' => 'is_numeric', + 'object' => 'is_object', + 'real' => 'is_real', + 'resource' => 'is_resource', + 'scalar' => 'is_scalar', + 'string' => 'is_string', + ]; + + /** @var string The passed argument in construct */ + private $typeSome; + + /** @var string The dectected primitive type */ + private $primitive; + + /** @var string The detected class name (if any) */ + private $className; + + /** @var string The detected interface name (if any) */ + private $interfaceName; + + /** + * @var string a primitive type, class name or interface + */ + public function __construct(string $typeSome) + { + $this->typeSome = $typeSome; + $this->setPrimitive(); + } + + public function typeString(): string + { + return $this->className ?? $this->interfaceName ?? $this->primitive; + } + + public function primitive(): string + { + return $this->primitive; + } + + public function className(): string + { + return $this->className; + } + + public function interfaceName(): string + { + return $this->interfaceName; + } + + public function validate(object $object): bool + { + $objectClass = get_class($object); + switch (true) { + case isset($this->className, $this->interfaceName): + return $this->isClassName($objectClass) || $this->isInterfaceInstance($object); + case isset($this->className): + return $this->isClassName($objectClass); + case isset($this->interfaceName): + return $this->isInterfaceInstance($object); + } + + return false; + } + + public function validatePrimitive($var): bool + { + return gettype($var) == $this->primitive; + } + + public function validator(): callable + { + return static::TYPE_VALIDATORS[$this->primitive]; + } + + private function isClassName(string $objectClass): bool + { + return $objectClass == $this->className; + } + + private function isInterfaceInstance(object $object): bool + { + return $object instanceof $this->interfaceName; + } + + private function setPrimitive(): void + { + if (isset(static::TYPE_VALIDATORS[$this->typeSome])) { + $this->primitive = $this->typeSome; + } else { + $this->handleClassName(); + $this->handleInterfaceName(); + if (isset($this->className) || isset($this->interfaceName)) { + $this->primitive = 'object'; + } + } + } + + private function handleClassName(): void + { + if (class_exists($this->typeSome)) { + $this->className = $this->typeSome; + } + } + + private function handleInterfaceName(): void + { + if (interface_exists($this->typeSome)) { + $this->interfaceName = $this->typeSome; + } + } +} diff --git a/Chevereto-Chevere/src/Utility/Arr.php b/Chevereto-Chevere/src/Utility/Arr.php new file mode 100644 index 000000000..f529122fa --- /dev/null +++ b/Chevereto-Chevere/src/Utility/Arr.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Utility; + +/** + * Array handling and transformation utils. + */ +abstract class Arr +{ + const FILTER_EXCLUSION = 'filter_exclusion'; + const FILTER_REMOVE = 'filter_remove'; + const DEFAULT_FILTER = self::FILTER_EXCLUSION; + + /** + * Filter array with another array. + * Useful to quickly filter an array using another array as the filter. + * + * @param array $array source array to be filtered (key => value) + * @param array $keys array keys to filter (keys as comma-separated values) + * @param string $mode Mode to filter the array: + * Utility\Arr::FILTER_EXCLUSION grabs filter values from source array. + * Utility\Arr::FILTER_REMOVE removes filter values from the source array. + * + * @return array the filtered array + */ + public static function filterArray(array $array, array $keys, $mode = self::FILTER_EXCLUSION): array + { + $return = []; + foreach ($keys as $k => $v) { + switch ($mode) { + default: + case static::DEFAULT_FILTER: + if (!array_key_exists($v, $array)) { + break; + } + $return[$v] = $array[$v]; + break; + case static::FILTER_REMOVE: + unset($array[$v]); + break; + } + } + + return $mode == static::DEFAULT_FILTER ? $return : $array; + } + + /** + * UTF-8 encode an array (recursive). + * + * @param array $array array to be encoded to UTF-8 + * + * @return array UTF-8 encoded array + */ + public static function utf8Encode(array &$array): array + { + array_walk_recursive($array, function (&$val) { + $val = mb_convert_encoding($val, 'UTF-8', mb_detect_encoding($val)); + }); + + return $array; + } + + /** + * Remove empty properties from an array (recursive). + * + * @param array $array array to be cleaned + * + * @return array an array without empty properties + */ + public static function removeEmpty(array $array): array + { + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = static::removeEmpty($array[$key]); + } + if (empty($array[$key])) { + unset($array[$key]); + } + } + + return $array; + } + + /** + * Find all combinations of sets in an array, also called the power set. + * + * If you pass a subject, it enabled the generation for creating multiple sets from a subject string. + * + * @see https://www.oreilly.com/library/view/php-cookbook/1565926811/ch04s25.html + * + * @param array $array array to determine its power set + * @param bool $preserveKeys true to preserve keys on power set + * + * @return array array power set + */ + public static function powerSet(array $array, bool $preserveKeys = false): array + { + $sets = []; + $sets[] = $preserveKeys ? array_fill(0, count($array), null) : []; + foreach ($array as $k => $element) { + foreach ($sets as $combination) { + if ($preserveKeys) { + $set = $combination; + $set[$k] = $element; + ksort($set); + } else { + $set = array_merge($combination, [$element]); + } + + $sets[] = $set; + } + } + + return $sets; + } +} diff --git a/Chevereto-Chevere/src/Utility/Benchmark.php b/Chevereto-Chevere/src/Utility/Benchmark.php new file mode 100644 index 000000000..8c7c46a91 --- /dev/null +++ b/Chevereto-Chevere/src/Utility/Benchmark.php @@ -0,0 +1,300 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Utility; + +use LogicException; +use Chevere\Message; +use Chevere\Traits\PrintableTrait; + +// TODO: Needs console output + +/** + * Benchmark provides a simple way to determine which code procedure perform faster compared to others. + */ +// $benchmark = (new Benchmark(1000, 30)) +// ->arguments(1, 3) +// ->add(function (int $a, int $b) { +// return $a + $b; +// }, 'Sum') +// ->add(function (int $a, int $b) { +// return $a/$b; +// }, 'Division') +// ->add(function (int $a, int $b) { +// return $a * $b; +// }, 'Multiply'); +// print $benchmark; +final class Benchmark +{ + use PrintableTrait; + + /** @var int Determines the number of colums used for output. */ + const COLUMNS = 50; + + private $columns; + private $callables; + private $arguments; + private $index; + private $unnammedCnt; + private $totalCnt; + private $time; + protected $printable; + private $maxExecutionTime; + private $constructTime; + private $requestTime; + private $lineSeparator; + private $timeTakenReadable; + private $times; + private $timeLimit = null; + + /** @var array */ + private $results; + + /** @var array */ + private $res; + + /** @var int */ + private $runs; + + /** @var bool */ + private $isAborted; + + /** @var bool */ + private $isPHPAborted; + + /** @var bool */ + private $isSelfAborted; + + /** @var float */ + private $startTimestamp; + + /** + * @param int $times number of times to run each function + * @param int $timeLimit time limit for this benchmark, in seconds + */ + public function __construct(int $times, int $timeLimit = null) + { + $this->constructTime = microtime(true); // - - $this->requestTime + $this->maxExecutionTime = ini_get('max_execution_time'); + $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?: 0; + $this->times = $times; + $this->totalCnt = 0; + if (null !== $timeLimit) { + $this->timeLimit = $timeLimit; + } + $this->columns = (int) static::COLUMNS; + } + + /** + * Set the callable arguments. + * + * @return self + */ + public function arguments(): self + { + $this->arguments = func_get_args(); + + return $this; + } + + /** + * Add a callable to the benchmark queue. + * + * @param callable $callable callable + * @param string $name callable name, or alias for your own reference + * + * @return self + */ + public function add(callable $callable, string $name = null): self + { + if (!isset($name)) { + if (null == $this->unnammedCnt) { + $this->unnammedCnt = '1'; + } + $name = 'Unnammed#' . $this->unnammedCnt; + ++$this->unnammedCnt; + } + if (null != $this->callables && array_key_exists($name, $this->callables)) { + throw new LogicException( + (new Message('Duplicate callable declaration %s')) + ->code('%s', $name) + ->toString() + ); + } + ++$this->totalCnt; + $this->callables[$name] = $callable; + + return $this; + } + + /** + * Run the benchmark. + */ + public function exec(): void + { + $this->index = []; + $this->results = []; + $this->isAborted = false; + $this->isPHPAborted = false; + $this->isSelfAborted = false; + $this->startTimestamp = microtime(true); + $this->handleCallables(); + $this->processCallablesStats(); + $title = __CLASS__ . ' results'; + $border = 1; + $lineChar = '-'; + $this->lineSeparator = str_repeat($lineChar, $this->columns); + $pad = (int) round(($this->columns - (strlen($title) + $border)) / 2, 0); + $head = '|' . str_repeat(' ', $pad) . $title . str_repeat(' ', floor($pad) == $pad ? ($pad - 1) : $pad) . '|'; + $this->res = [ + $this->lineSeparator, + $head, + $this->lineSeparator, + 'Start: ' . DateTime::getUTC(), + 'Hostname: ' . gethostname(), + 'PHP version: ' . phpversion(), + 'Server: ' . php_uname('s') . ' ' . php_uname('r') . ' ' . php_uname('m'), + $this->lineSeparator, + ]; + $this->processResults(); + $this->handleAbortedRes(); + $this->timeTakenReadable = ' Time taken: ' . round($this->time, 4) . ' s'; + $this->res[] = str_repeat(' ', (int) max(0, $this->columns - strlen($this->timeTakenReadable))) . $this->timeTakenReadable; + $this->printable = '
' . implode("\n", $this->res) . '
'; + } + + /** + * Get bcrypt optimal cost. + * + * @param float $time seconds to use for this test + * @param int $cost cost to be used as starting value + * + * @return int optimal BCrypt cost + */ + public static function bcryptCost(float $time = 0.1, int $cost = 9): int + { + do { + ++$cost; + $ti = microtime(true); + password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]); + $tf = microtime(true); + } while (($tf - $ti) < $time); + + return $cost; + } + + private function handleCallables(): void + { + foreach ($this->callables as $k => $v) { + if ($this->isAborted) { + $this->time = microtime(true) - $this->startTimestamp; + break; + } + $timeInit = microtime(true); + $this->runs = 0; + $this->runCallable($v); + $timeFinish = microtime(true); + $timeTaken = floatval($timeFinish - $timeInit); + $this->index[$k] = $timeTaken; + $this->results[$k] = [ + 'time' => $timeTaken, + 'runs' => $this->runs, + // 'adds' => '', + ]; + } + } + + private function runCallable(callable $callable): void + { + for ($i = 0; $i < $this->times; ++$i) { + $this->isPHPAborted = !$this->canPHPKeepGoing(); + $this->isSelfAborted = !$this->canSelfKeepGoing(); + if ($this->isPHPAborted || $this->isSelfAborted) { + $this->isAborted = true; + break; + } + $callable(...($this->arguments ?: [])); + ++$this->runs; + } + ++$i; + if ($i == $this->totalCnt) { + $this->time = microtime(true) - $this->startTimestamp; + } + } + + private function processCallablesStats(): void + { + asort($this->index); + if (count($this->index) > 1) { + foreach ($this->index as $k => $v) { + $timeTaken = $this->results[$k]['time']; + if (!isset($fastestTime)) { + $fastestTime = $timeTaken; + } else { + $this->results[$k]['adds'] = round(100 * (($timeTaken - $fastestTime) / $fastestTime), 2) . '%'; + } + } + } + } + + private function processResults(): void + { + $i = 0; + foreach ($this->index as $name => $time) { + ++$i; + $resultTitle = $name; + $result = $this->results[$name]; + if (1 == $i) { + if (count($this->index) > 1) { + $resultTitle .= ' (fastest)'; + } + } else { + $resultTitle .= ' (' . $result['adds'] . ' slower)'; + } + $this->res[] = $resultTitle; + $resRuns = Number::abbreviate($result['runs']) . ' runs'; + $resRuns .= ' in ' . round($result['time'], 4) . ' s'; + if ($result['runs'] != $this->times) { + $resRuns .= ' ~ missed ' . ($this->times - $result['runs']) . ' runs'; + } + $this->res[] = $resRuns; + $this->res[] = $this->lineSeparator; + } + } + + private function handleAbortedRes(): void + { + if ($this->isAborted) { + $this->res[] = 'Note: Process aborted (' . ($this->isPHPAborted ? 'PHP' : 'self') . ' time limit)'; + $this->res[] = $this->lineSeparator; + } + } + + private function canSelfKeepGoing(): bool + { + if (null != $this->timeLimit && microtime(true) - $this->constructTime > $this->timeLimit) { + return false; + } + + return true; + } + + private function canPHPKeepGoing(): bool + { + if (0 != $this->maxExecutionTime && microtime(true) - $this->requestTime > $this->maxExecutionTime) { + return false; + } + + return true; + } +} diff --git a/Chevereto-Chevere/src/Utility/Bytes.php b/Chevereto-Chevere/src/Utility/Bytes.php new file mode 100644 index 000000000..54f583939 --- /dev/null +++ b/Chevereto-Chevere/src/Utility/Bytes.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Utility; + +abstract class Bytes +{ + const UNITS = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + /** + * Returns bytes from size + suffix format. + * + * @param string $size bytes to be formatted, like "2 MB" + * @param int $cut indicates the chop needed to substract the byte-unit + * + * @return int byte representation + */ + public static function get(string $size, int $cut = -2): int + { + $suffix = strtoupper(substr($size, $cut)); + if (strlen($suffix) == 1) { + $iniToSuffix = ['M' => 'MB', 'G' => 'GB']; + $suffix = $iniToSuffix[$suffix]; + } + $value = intval($size); + if (!in_array($suffix, static::UNITS)) { + return $value; + } + + $array_search = array_search($suffix, static::UNITS); + if (false !== $array_search) { + $powFactor = (int) $array_search + 1; + } else { + $powFactor = 0; + } + + return (int) ($value * pow(1000, $powFactor)); + } + + /** + * Converts bytes to human readable representation. + * + * @param string $bytes bytes to be formatted + * @param int $round how many decimals you want to get, default 1 + * + * @return string formatted size string like 10 MB + */ + public static function format($bytes, $round = 1): ?string + { + if (!is_numeric($bytes)) { + return null; + } + if ($bytes < 1000) { + return "$bytes B"; + } + foreach (static::UNITS as $k => $v) { + $multiplier = pow(1000, $k + 1); + $threshold = $multiplier * 1000; + if ($bytes < $threshold) { + $size = round($bytes / $multiplier, $round); + + return "$size $v"; + } + } + } + + /** + * Converts bytes to MB. + * + * @param int $bytes bytes to be formatted + * + * @return float MB representation + */ + public static function toMB(int $bytes): float + { + return round($bytes / pow(10, 6)); + } + + /** + * Get bytes from the php.ini values. + * + * Allows to get a byte representation from php.ini values which uses short format notation. + * + * @param string $size short size notation (like "2M") + * + * @return float byte representation + */ + public static function getIni(string $size): float + { + return static::get($size, -1); + } +} diff --git a/Chevereto-Chevere/src/Utility/Color.php b/Chevereto-Chevere/src/Utility/Color.php new file mode 100644 index 000000000..4e8505b2b --- /dev/null +++ b/Chevereto-Chevere/src/Utility/Color.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Hacer objeto servicio? new Color(string, Color::RGB)->toHex() +// Combo object + static + +namespace Chevere\Utility; + +use InvalidArgumentException; +use Chevere\Message; + +abstract class Color +{ + /** + * Converts HEX to RGB (color). + * http://bavotasan.com/2011/convert-hex-color-to-rgb-using-php/. + * + * @param string $hex hexadecimal color representation + * @param bool $array TRUE to get array return [R,G,B] + * + * @return mixed Array with RGB values [R,G,B] or string like rgb(R,G,B). + * + * TODO: typehint return + */ + public static function hexToRgb(string $hex, bool $array = true) + { + $hex = str_replace('#', '', $hex); + if (3 == strlen($hex)) { + $r = hexdec(substr($hex, 0, 1).substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1).substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1).substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + $rgb = [$r, $g, $b]; + + return $array ? $rgb : ('rgb('.implode(',', $rgb).')'); + } + + /** + * Converts RGB to HEX (color). + * + * @param mixed $rgb RGB color representation array [R,G,B] or rgb(r,g,b) + * + * @return string HEX value including the number sign (#) + */ + public static function rgbToHex($rgb): ?string + { + $type = gettype($rgb); + switch ($type) { + case 'string': + $val = preg_replace('/[^\d,]/', '', $rgb); + if (is_string($val)) { + $val = explode(',', $val); + } else { + return null; + } + break; + case 'array': + $val = $rgb; + break; + default: + throw new InvalidArgumentException( + (new Message('Only %s and %a types can be used with this function (type %t provided)')) + ->code('%s', 'string') + ->code('%a', 'array') + ->code('%t', $type) + ->toString() + ); + } + $hex = '#'; + $hex .= str_pad(dechex($val[0]), 2, '0', STR_PAD_LEFT); + $hex .= str_pad(dechex($val[1]), 2, '0', STR_PAD_LEFT); + $hex .= str_pad(dechex($val[2]), 2, '0', STR_PAD_LEFT); + + return $hex; + } +} diff --git a/Chevereto-Chevere/src/Utility/DateTime.php b/Chevereto-Chevere/src/Utility/DateTime.php new file mode 100644 index 000000000..0b9fff765 --- /dev/null +++ b/Chevereto-Chevere/src/Utility/DateTime.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Utility; + +use DateTimeInterface; +use InvalidArgumentException; + +// $var = new DateTime('2016-5-05'); +// $var = '2017-12-05 01:02:03'; +// $date = '2016-12-4 01:02:03'; +// $dates = []; +// for($i=0;$i<730;$i++) { +// $dates[] = (new DateTime($date))->modify("+$i day")->format(DateTime::SQL); +// } +// $dates[] = '2018-05-06 19:54:00'; +// foreach($dates as $k => $v) { +// $to = (new DateTime($v))->timeAgo(); +// dump($v, $to); +// } +// $res = (new DateTime('2015-05-05 00:00:00'))->timeBetween('2015-05-05 00:40:30', 's'); +// // +// $res = (new DateTime($var))->add(new \DateInterval('PT2S')); +// $res = (new DateTime($var))->modify('-2 years, -2 seconds')->format(DateTime::ATOM); +// dump($var, $res, $to); + +class DateTime extends \DateTime implements DateTimeInterface +{ + const UNIT_HOUR = 'h'; + const UNIT_MINUTE = 'i'; + const UNIT_SECOND = 's'; + const UNIT_DAY = 'd'; + const UNIT_WEEK = 'w'; + const UNIT_MONTH = 'm'; + const UNIT_YEAR = 'y'; + + const UNITS = [ + self::UNIT_HOUR, + self::UNIT_MINUTE, + self::UNIT_SECOND, + self::UNIT_DAY, + self::UNIT_WEEK, + self::UNIT_MONTH, + self::UNIT_YEAR, + ]; + + const UNITS_READABLE = [ + self::UNIT_SECOND => 'second', + self::UNIT_MINUTE => 'minute', + self::UNIT_HOUR => 'hour', + self::UNIT_DAY => 'day', + self::UNIT_WEEK => 'week', + self::UNIT_MONTH => 'month', + self::UNIT_YEAR => 'year', + ]; + + const SQL = 'Y-m-d H:i:s'; + + // El pueblinski minski está morinski de hambrinski! + const MATRIOSHKA = [ + self::UNIT_YEAR => 1, + self::UNIT_MONTH => 1 / 12, + self::UNIT_WEEK => 1 / 4, + self::UNIT_DAY => 1 / 7, + self::UNIT_HOUR => 1 / 24, + self::UNIT_MINUTE => 1 / 60, + self::UNIT_SECOND => 1 / 60, + ]; + + const MINUTE_SECONDS = 60; + const HOUR_SECONDS = 3600; + const DAY_SECONDS = 86400; + const WEEK_SECONDS = 604800; + const MONTH_SECONDS = 2629750; + const YEAR_SECONDS = 31556900; + + const SECONDS_TABLE = [ + self::UNIT_SECOND => 1, + self::UNIT_MINUTE => self::MINUTE_SECONDS, + self::UNIT_HOUR => self::HOUR_SECONDS, + self::UNIT_DAY => self::DAY_SECONDS, + self::UNIT_WEEK => self::WEEK_SECONDS, + self::UNIT_MONTH => self::MONTH_SECONDS, + self::UNIT_YEAR => self::YEAR_SECONDS, + ]; + + /** + * Get datetime UTC ATOM (RFC3339). + * + * @param string $format date format to use + * + * @return string datetime UTC + */ + public static function getUTC(string $format = self::ATOM): string + { + return gmdate($format); + } + + /** + * Format datetime as MySQL datetime format. + * + * @param string $datetime timestamp + * + * @return string datetime MySQL (YYYY-MM-DD HH:MM:SS) + */ + public static function formatSQL(string $datetime = null): string + { + if (!isset($datetime)) { + return static::getUTC(static::SQL); + } + + return (new self($datetime))->format(static::SQL); + } + + /** + * Returns the difference between two dates in the target time unit. + * Useful to compare datetimes in a given unit. + * + * @param string $datetime datetime + * @param string $unit time unit (default 's') [s;i;h;d;w;m;y] + * + * @return float time diff between the two datetimes + */ + public function timeBetween(string $datetime, string $unit = self::UNIT_SECOND): float + { + if (!isset(static::SECONDS_TABLE[$unit])) { + throw new InvalidArgumentException("Unexpected unit $unit, you can only use one of the following units: " . implode(', ', static::UNITS) . ''); + } + $then = new self($datetime); + $diff = abs($then->getTimestamp() - $this->getTimestamp()); // In seconds + return 0 == $diff ? 0 : floatval($diff / static::SECONDS_TABLE[$unit]); + } +} diff --git a/Chevereto-Chevere/src/Utility/Number.php b/Chevereto-Chevere/src/Utility/Number.php new file mode 100644 index 000000000..93882d6a5 --- /dev/null +++ b/Chevereto-Chevere/src/Utility/Number.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Utility; + +abstract class Number +{ + /** + * Abbreviate a number adding its alpha suffix. + * + * @param int $number number to be abbreviated + * @param int $precision round precision + * + * @return string Abbreviated number (ie. 2K or 1M). + */ + public static function abbreviate(int $number, int $precision = 0): string + { + if ($number != 0) { + $abbreviations = [ + 24 => 'Y', + 21 => 'Z', + 18 => 'E', + 15 => 'P', + 12 => 'T', + 9 => 'B', + 6 => 'M', + 3 => 'K', + 0 => null, + ]; + foreach ($abbreviations as $exponent => $abbreviation) { + if ($number >= pow(10, $exponent)) { + $div = $number / pow(10, $exponent); + $float = floatval($div); + $number = null === $abbreviation ? (string) $float : (round($float, $precision) . $abbreviation); + break; + } + } + } + + return (string) $number; + } + + /** + * Converts a fraction into a decimal (float). + * + * @param string $fraction a fraction number (like 1/25) + */ + public static function fractionToDecimal($fraction): ?float + { + [$top, $bottom] = explode('/', $fraction); + + return (float) ($bottom == 0 ? $fraction : ($top / $bottom)); + } +} diff --git a/Chevereto-Chevere/src/Utility/Random.php b/Chevereto-Chevere/src/Utility/Random.php new file mode 100644 index 000000000..f2fd4db6f --- /dev/null +++ b/Chevereto-Chevere/src/Utility/Random.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Utility; + +abstract class Random +{ + /** + * Generate random numeric values within a limited range. + * + * @param int $min minimum number + * @param int $max maximum number + * @param int $limit number of random values that should be generated + * + * @return array an array with the generated random values + */ + public static function numericValues(int $min, int $max, int $limit): array + { + // Get the accurate min and max + $min = min($min, $max); + $max = max($min, $max); + // Go home + if ($min == $max) { + return [$min]; + } + // is the limit ok? + $maxLimit = abs($max - $min); + if ($limit > $maxLimit) { + $limit = $maxLimit; + } + $array = []; + for ($i = 0; $i < $limit; ++$i) { + $rand = rand($min, $max); + while (in_array($rand, $array)) { + $rand = random_int($min, $max); + } + $array[$i] = $rand; + } + + return $array; + } + + /** + * Generate a random string. + * + * @param int $length length of the generated random string + * + * @return string random string + */ + public static function string(int $length = 8): string + { + $bytes = random_bytes($length); + + return bin2hex($bytes); + } +} diff --git a/Chevereto-Chevere/src/Utility/Str.php b/Chevereto-Chevere/src/Utility/Str.php new file mode 100644 index 000000000..ad23e0bad --- /dev/null +++ b/Chevereto-Chevere/src/Utility/Str.php @@ -0,0 +1,385 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\Utility; + +abstract class Str +{ + const TRANSLITERATION = [ + 'IJ' => 'I', 'Ö' => 'O', 'Œ' => 'O', 'Ü' => 'U', 'ä' => 'a', 'æ' => 'a', + 'ij' => 'i', 'ö' => 'o', 'œ' => 'o', 'ü' => 'u', 'ß' => 's', 'ſ' => 's', + 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', + 'Æ' => 'A', 'Ā' => 'A', 'Ą' => 'A', 'Ă' => 'A', 'Ç' => 'C', 'Ć' => 'C', + 'Č' => 'C', 'Ĉ' => 'C', 'Ċ' => 'C', 'Ď' => 'D', 'Đ' => 'D', 'È' => 'E', + 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ē' => 'E', 'Ę' => 'E', 'Ě' => 'E', + 'Ĕ' => 'E', 'Ė' => 'E', 'Ĝ' => 'G', 'Ğ' => 'G', 'Ġ' => 'G', 'Ģ' => 'G', + 'Ĥ' => 'H', 'Ħ' => 'H', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', + 'Ī' => 'I', 'Ĩ' => 'I', 'Ĭ' => 'I', 'Į' => 'I', 'İ' => 'I', 'Ĵ' => 'J', + 'Ķ' => 'K', 'Ľ' => 'K', 'Ĺ' => 'K', 'Ļ' => 'K', 'Ŀ' => 'K', 'Ł' => 'L', + 'Ñ' => 'N', 'Ń' => 'N', 'Ň' => 'N', 'Ņ' => 'N', 'Ŋ' => 'N', 'Ò' => 'O', + 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ø' => 'O', 'Ō' => 'O', 'Ő' => 'O', + 'Ŏ' => 'O', 'Ŕ' => 'R', 'Ř' => 'R', 'Ŗ' => 'R', 'Ś' => 'S', 'Ş' => 'S', + 'Ŝ' => 'S', 'Ș' => 'S', 'Š' => 'S', 'Ť' => 'T', 'Ţ' => 'T', 'Ŧ' => 'T', + 'Ț' => 'T', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ū' => 'U', 'Ů' => 'U', + 'Ű' => 'U', 'Ŭ' => 'U', 'Ũ' => 'U', 'Ų' => 'U', 'Ŵ' => 'W', 'Ŷ' => 'Y', + 'Ÿ' => 'Y', 'Ý' => 'Y', 'Ź' => 'Z', 'Ż' => 'Z', 'Ž' => 'Z', 'à' => 'a', + 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ā' => 'a', 'ą' => 'a', 'ă' => 'a', + 'å' => 'a', 'ç' => 'c', 'ć' => 'c', 'č' => 'c', 'ĉ' => 'c', 'ċ' => 'c', + 'ď' => 'd', 'đ' => 'd', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', + 'ē' => 'e', 'ę' => 'e', 'ě' => 'e', 'ĕ' => 'e', 'ė' => 'e', 'ƒ' => 'f', + 'ĝ' => 'g', 'ğ' => 'g', 'ġ' => 'g', 'ģ' => 'g', 'ĥ' => 'h', 'ħ' => 'h', + 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ī' => 'i', 'ĩ' => 'i', + 'ĭ' => 'i', 'į' => 'i', 'ı' => 'i', 'ĵ' => 'j', 'ķ' => 'k', 'ĸ' => 'k', + 'ł' => 'l', 'ľ' => 'l', 'ĺ' => 'l', 'ļ' => 'l', 'ŀ' => 'l', 'ñ' => 'n', + 'ń' => 'n', 'ň' => 'n', 'ņ' => 'n', 'ʼn' => 'n', 'ŋ' => 'n', 'ò' => 'o', + 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ø' => 'o', 'ō' => 'o', 'ő' => 'o', + 'ŏ' => 'o', 'ŕ' => 'r', 'ř' => 'r', 'ŗ' => 'r', 'ś' => 's', 'š' => 's', + 'ť' => 't', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ū' => 'u', 'ů' => 'u', + 'ű' => 'u', 'ŭ' => 'u', 'ũ' => 'u', 'ų' => 'u', 'ŵ' => 'w', 'ÿ' => 'y', + 'ý' => 'y', 'ŷ' => 'y', 'ż' => 'z', 'ź' => 'z', 'ž' => 'z', 'Α' => 'A', + 'Ά' => 'A', 'Ἀ' => 'A', 'Ἁ' => 'A', 'Ἂ' => 'A', 'Ἃ' => 'A', 'Ἄ' => 'A', + 'Ἅ' => 'A', 'Ἆ' => 'A', 'Ἇ' => 'A', 'ᾈ' => 'A', 'ᾉ' => 'A', 'ᾊ' => 'A', + 'ᾋ' => 'A', 'ᾌ' => 'A', 'ᾍ' => 'A', 'ᾎ' => 'A', 'ᾏ' => 'A', 'Ᾰ' => 'A', + 'Ᾱ' => 'A', 'Ὰ' => 'A', 'ᾼ' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', + 'Ε' => 'E', 'Έ' => 'E', 'Ἐ' => 'E', 'Ἑ' => 'E', 'Ἒ' => 'E', 'Ἓ' => 'E', + 'Ἔ' => 'E', 'Ἕ' => 'E', 'Ὲ' => 'E', 'Ζ' => 'Z', 'Η' => 'I', 'Ή' => 'I', + 'Ἠ' => 'I', 'Ἡ' => 'I', 'Ἢ' => 'I', 'Ἣ' => 'I', 'Ἤ' => 'I', 'Ἥ' => 'I', + 'Ἦ' => 'I', 'Ἧ' => 'I', 'ᾘ' => 'I', 'ᾙ' => 'I', 'ᾚ' => 'I', 'ᾛ' => 'I', + 'ᾜ' => 'I', 'ᾝ' => 'I', 'ᾞ' => 'I', 'ᾟ' => 'I', 'Ὴ' => 'I', 'ῌ' => 'I', + 'Θ' => 'T', 'Ι' => 'I', 'Ί' => 'I', 'Ϊ' => 'I', 'Ἰ' => 'I', 'Ἱ' => 'I', + 'Ἲ' => 'I', 'Ἳ' => 'I', 'Ἴ' => 'I', 'Ἵ' => 'I', 'Ἶ' => 'I', 'Ἷ' => 'I', + 'Ῐ' => 'I', 'Ῑ' => 'I', 'Ὶ' => 'I', 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', + 'Ν' => 'N', 'Ξ' => 'K', 'Ο' => 'O', 'Ό' => 'O', 'Ὀ' => 'O', 'Ὁ' => 'O', + 'Ὂ' => 'O', 'Ὃ' => 'O', 'Ὄ' => 'O', 'Ὅ' => 'O', 'Ὸ' => 'O', 'Π' => 'P', + 'Ρ' => 'R', 'Ῥ' => 'R', 'Σ' => 'S', 'Τ' => 'T', 'Υ' => 'Y', 'Ύ' => 'Y', + 'Ϋ' => 'Y', 'Ὑ' => 'Y', 'Ὓ' => 'Y', 'Ὕ' => 'Y', 'Ὗ' => 'Y', 'Ῠ' => 'Y', + 'Ῡ' => 'Y', 'Ὺ' => 'Y', 'Φ' => 'F', 'Χ' => 'X', 'Ψ' => 'P', 'Ω' => 'O', + 'Ώ' => 'O', 'Ὠ' => 'O', 'Ὡ' => 'O', 'Ὢ' => 'O', 'Ὣ' => 'O', 'Ὤ' => 'O', + 'Ὥ' => 'O', 'Ὦ' => 'O', 'Ὧ' => 'O', 'ᾨ' => 'O', 'ᾩ' => 'O', 'ᾪ' => 'O', + 'ᾫ' => 'O', 'ᾬ' => 'O', 'ᾭ' => 'O', 'ᾮ' => 'O', 'ᾯ' => 'O', 'Ὼ' => 'O', + 'ῼ' => 'O', 'α' => 'a', 'ά' => 'a', 'ἀ' => 'a', 'ἁ' => 'a', 'ἂ' => 'a', + 'ἃ' => 'a', 'ἄ' => 'a', 'ἅ' => 'a', 'ἆ' => 'a', 'ἇ' => 'a', 'ᾀ' => 'a', + 'ᾁ' => 'a', 'ᾂ' => 'a', 'ᾃ' => 'a', 'ᾄ' => 'a', 'ᾅ' => 'a', 'ᾆ' => 'a', + 'ᾇ' => 'a', 'ὰ' => 'a', 'ᾰ' => 'a', 'ᾱ' => 'a', 'ᾲ' => 'a', 'ᾳ' => 'a', + 'ᾴ' => 'a', 'ᾶ' => 'a', 'ᾷ' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', + 'ε' => 'e', 'έ' => 'e', 'ἐ' => 'e', 'ἑ' => 'e', 'ἒ' => 'e', 'ἓ' => 'e', + 'ἔ' => 'e', 'ἕ' => 'e', 'ὲ' => 'e', 'ζ' => 'z', 'η' => 'i', 'ή' => 'i', + 'ἠ' => 'i', 'ἡ' => 'i', 'ἢ' => 'i', 'ἣ' => 'i', 'ἤ' => 'i', 'ἥ' => 'i', + 'ἦ' => 'i', 'ἧ' => 'i', 'ᾐ' => 'i', 'ᾑ' => 'i', 'ᾒ' => 'i', 'ᾓ' => 'i', + 'ᾔ' => 'i', 'ᾕ' => 'i', 'ᾖ' => 'i', 'ᾗ' => 'i', 'ὴ' => 'i', 'ῂ' => 'i', + 'ῃ' => 'i', 'ῄ' => 'i', 'ῆ' => 'i', 'ῇ' => 'i', 'θ' => 't', 'ι' => 'i', + 'ί' => 'i', 'ϊ' => 'i', 'ΐ' => 'i', 'ἰ' => 'i', 'ἱ' => 'i', 'ἲ' => 'i', + 'ἳ' => 'i', 'ἴ' => 'i', 'ἵ' => 'i', 'ἶ' => 'i', 'ἷ' => 'i', 'ὶ' => 'i', + 'ῐ' => 'i', 'ῑ' => 'i', 'ῒ' => 'i', 'ῖ' => 'i', 'ῗ' => 'i', 'κ' => 'k', + 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => 'k', 'ο' => 'o', 'ό' => 'o', + 'ὀ' => 'o', 'ὁ' => 'o', 'ὂ' => 'o', 'ὃ' => 'o', 'ὄ' => 'o', 'ὅ' => 'o', + 'ὸ' => 'o', 'π' => 'p', 'ρ' => 'r', 'ῤ' => 'r', 'ῥ' => 'r', 'σ' => 's', + 'ς' => 's', 'τ' => 't', 'υ' => 'y', 'ύ' => 'y', 'ϋ' => 'y', 'ΰ' => 'y', + 'ὐ' => 'y', 'ὑ' => 'y', 'ὒ' => 'y', 'ὓ' => 'y', 'ὔ' => 'y', 'ὕ' => 'y', + 'ὖ' => 'y', 'ὗ' => 'y', 'ὺ' => 'y', 'ῠ' => 'y', 'ῡ' => 'y', 'ῢ' => 'y', + 'ῦ' => 'y', 'ῧ' => 'y', 'φ' => 'f', 'χ' => 'x', 'ψ' => 'p', 'ω' => 'o', + 'ώ' => 'o', 'ὠ' => 'o', 'ὡ' => 'o', 'ὢ' => 'o', 'ὣ' => 'o', 'ὤ' => 'o', + 'ὥ' => 'o', 'ὦ' => 'o', 'ὧ' => 'o', 'ᾠ' => 'o', 'ᾡ' => 'o', 'ᾢ' => 'o', + 'ᾣ' => 'o', 'ᾤ' => 'o', 'ᾥ' => 'o', 'ᾦ' => 'o', 'ᾧ' => 'o', 'ὼ' => 'o', + 'ῲ' => 'o', 'ῳ' => 'o', 'ῴ' => 'o', 'ῶ' => 'o', 'ῷ' => 'o', 'А' => 'A', + 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'E', + 'Ж' => 'Z', 'З' => 'Z', 'И' => 'I', 'Й' => 'I', 'К' => 'K', 'Л' => 'L', + 'М' => 'M', 'Н' => 'N', 'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S', + 'Т' => 'T', 'У' => 'U', 'Ф' => 'F', 'Х' => 'K', 'Ц' => 'T', 'Ч' => 'C', + 'Ш' => 'S', 'Щ' => 'S', 'Ы' => 'Y', 'Э' => 'E', 'Ю' => 'Y', 'Я' => 'Y', + 'а' => 'A', 'б' => 'B', 'в' => 'V', 'г' => 'G', 'д' => 'D', 'е' => 'E', + 'ё' => 'E', 'ж' => 'Z', 'з' => 'Z', 'и' => 'I', 'й' => 'I', 'к' => 'K', + 'л' => 'L', 'м' => 'M', 'н' => 'N', 'о' => 'O', 'п' => 'P', 'р' => 'R', + 'с' => 'S', 'т' => 'T', 'у' => 'U', 'ф' => 'F', 'х' => 'K', 'ц' => 'T', + 'ч' => 'C', 'ш' => 'S', 'щ' => 'S', 'ы' => 'Y', 'э' => 'E', 'ю' => 'Y', + 'я' => 'Y', 'ð' => 'd', 'Ð' => 'D', 'þ' => 't', 'Þ' => 'T', 'ა' => 'a', + 'ბ' => 'b', 'გ' => 'g', 'დ' => 'd', 'ე' => 'e', 'ვ' => 'v', 'ზ' => 'z', + 'თ' => 't', 'ი' => 'i', 'კ' => 'k', 'ლ' => 'l', 'მ' => 'm', 'ნ' => 'n', + 'ო' => 'o', 'პ' => 'p', 'ჟ' => 'z', 'რ' => 'r', 'ს' => 's', 'ტ' => 't', + 'უ' => 'u', 'ფ' => 'p', 'ქ' => 'k', 'ღ' => 'g', 'ყ' => 'q', 'შ' => 's', + 'ჩ' => 'c', 'ც' => 't', 'ძ' => 'd', 'წ' => 't', 'ჭ' => 'c', 'ხ' => 'k', + 'ჯ' => 'j', 'ჰ' => 'h', 'ḩ' => 'h', 'ừ' => 'u', 'ế' => 'e', 'ả' => 'a', + 'ị' => 'i', 'ậ' => 'a', 'ệ' => 'e', 'ỉ' => 'i', 'ộ' => 'o', 'ồ' => 'o', + 'ề' => 'e', 'ơ' => 'o', 'ạ' => 'a', 'ẵ' => 'a', 'ư' => 'u', 'ắ' => 'a', + 'ằ' => 'a', 'ầ' => 'a', 'Ḩ' => 'H', 'Ḑ' => 'D', 'ḑ' => 'd', + 'ş' => 's', 'ţ' => 't', 'ễ' => 'e', + ]; + + /** + * Replace the first occurrence of the search string with the replacement + * string. + * + * @param string $search value being searched for + * @param string $replace replacement value that replaces found search + * values + * @param string $subject string being searched and replaced on + * + * @return string returns a string with the replaced value + */ + public static function replaceFirst(string $search, string $replace, string $subject): string + { + $pos = strpos($subject, $search); + if (false !== $pos) { + $subject = substr_replace($subject, $replace, $pos, strlen($search)); + } + + return $subject ?? ''; + } + + /** + * Replace the last occurrence of the search string with the replacement + * string. + * + * @param string $search value being searched for + * @param string $replace replacement value that replaces found search + * values + * @param string $subject string being searched and replaced on + * + * @return string returns a string with the replaced value + */ + public static function replaceLast(string $search, string $replace, string $subject): string + { + $pos = strrpos($subject, $search); + if (false !== $pos) { + $subject = substr_replace($subject, $replace, $pos, strlen($search)); + } + + return $subject ?? ''; + } + + /** + * Converts backslash into forward slashes. + * + * @param string $var path which will get forward slashes + */ + public static function forwardSlashes(string $var): string + { + return str_replace('\\', '/', $var); + } + + /** + * Detects if a string contains another sub-string. + * + * @param string $needle + * @param string $haystack + */ + public static function contains(string $needle, string $haystack): bool + { + return false !== strpos($haystack, $needle); + } + + /** + * Detects if a string begins with the given needle. + * + * @param string $needle value being searched for + * @param string $haystack string being searched + * + * @return bool TRUE if the haystack begins with the given needle + * + * @see http://php.net/manual/en/function.substr-compare.php + */ + public static function startsWith(string $needle, string $haystack): bool + { + $needleLen = strlen($needle); + if ($needleLen > strlen($haystack)) { + return false; + } + + return 0 === substr_compare($haystack, $needle, 0, $needleLen); + } + + /** + * Detects if a string ends with the given needle. + * + * @param string $needle value being searched for + * @param string $haystack string being searched + * + * @return bool TRUE if the haystack ends with the given needle + * + * @see http://php.net/manual/en/function.substr-compare.php + */ + public static function endsWith(string $needle, string $haystack): bool + { + $needleLen = strlen($needle); + if ($needleLen > strlen($haystack)) { + return false; + } + + return 0 === substr_compare($haystack, $needle, -$needleLen); + } + + public static function startsWithNumeric(string $haystack): bool + { + return strlen($haystack) > 0 && ctype_digit(substr($haystack, 0, 1)); + } + + /** + * A timing safe string equals comparison. + * + * To prevent leaking length information, it is important that user input is + * always used as the second parameter. + * + * @param string $safe internal (safe) value to be checked + * @param string $user user submitted (unsafe) value + * + * @return bool TRUE if the two strings are identical + */ + public static function compare(string $safe, string $user): bool + { + $safe .= chr(0); + $user .= chr(0); + $safeLen = strlen($safe); + $userLen = strlen($user); + $result = $safeLen - $userLen; + for ($i = 0; $i < $userLen; ++$i) { + $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i])); + } + // They are only identical strings if $result is exactly 0... + return 0 === $result; + } + + /** + * Truncate string. + * + * Truncates any given text based in either number of characters. + * + * Taken from http://www.chirp.com.au/ and improved for Chevereto. + * + * @param string $string string to be truncated + * @param int $limit integer for character lengh based trimming + * @param string $pad String to be appended to the truncated string, default "...". + * + * @return string truncated string + */ + public static function truncate(string $string, int $limit, string $pad = '...'): string + { + $encoding = 'UTF-8'; + if (mb_strlen($string, $encoding) <= $limit) { + return $string; + } + $string = trim(mb_substr($string, 0, $limit - strlen($pad), $encoding)) . $pad; + + return $string ?? ''; + } + + /** + * Shorthand for UTF-8 mb_strtolower. + * + * @param string $string + * + * @return string lowercased string + */ + public static function toLowercase(string $string): string + { + return mb_strtolower($string, 'UTF-8'); + } + + /** + * Strip non-alphanumeric chars from string. + * + * @param string $string + * + * @return string string without non-alphanumeric chars + */ + public static function stripNonAlnum(string $string): string + { + return preg_replace('/[^[:alnum:][:space:]]/u', '', $string) ?? ''; + } + + /** + * Strip whitespaces from string. + * + * @param string $string + * + * @return string string without whitespaces + */ + public static function stripWhitespace(string $string): string + { + return preg_replace('/\s+/', '', $string) ?? ''; + } + + /** + * Strip extra whitespace from string. + * + * @param string $string + * + * @return string string without extra whitespaces + */ + public static function stripExtraWhitespace(string $string): string + { + return preg_replace('/\s+/', ' ', $string) ?? ''; + } + + /** + * Remove all the accents of an string. + * + * Sanitizes (removes) all the accents present in a string so it allows to + * convert strings like 'ykésâêén' into 'ykesaeen'. + * + * @param string $string string to be sanitized + * + * @return string unaccented sanitized string + * + * @see http://www.evaisse.net/2008/php-translit-remove-accent-unaccent-21001 + */ + public static function unaccent(string $string): string + { + $string = (string) $string; + $encoding = mb_detect_encoding($string) ?: false; + $utf8 = 'utf-8' == $encoding; + if (!$utf8) { + $string = utf8_encode($string); + } + + $string = str_replace(array_keys(static::TRANSLITERATION), array_values(static::TRANSLITERATION), $string); + // Missing something? + $string = htmlentities($string, ENT_QUOTES, 'UTF-8'); + if (false !== strpos($string, '&')) { + $replaced = preg_replace('~&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|tilde|uml);~i', '$1', $string); + + return null == $replaced ? '' : html_entity_decode($replaced, ENT_QUOTES, 'UTF-8'); + } + + return $string ?? ''; + } + + /** + * Right-tail a string. + * + * Appends a string with a tail string, without repeats. + * + * @param string $string + * @param string $tail string tail + * + * @return string right-tailed string + */ + public static function rtail(string $string, string $tail): string + { + return rtrim($string, $tail) . $tail; + } + + /** + * Left-tail a string. + * + * Prepends a string with a tail string, without repeats. + * + * @param string $string + * @param string $tail string tail + * + * @return string right-tailed string + */ + public static function ltail(string $string, string $tail): string + { + return $tail . ltrim($string, $tail); + } +} diff --git a/Chevereto-Chevere/src/Validate.php b/Chevereto-Chevere/src/Validate.php new file mode 100644 index 000000000..8dcbd2bbe --- /dev/null +++ b/Chevereto-Chevere/src/Validate.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere; + +class Validate +{ + /** + * Fast way to check for valid timezone. + * + * @param string $timezone timezone id + * + * @return bool TRUE if $timezone is a valid timezone + */ + public static function timezone(string $timezone): bool + { + $return = false; + $list = timezone_abbreviations_list(); + foreach ($list as $zone) { + foreach ($zone as $item) { + $tz = $item['timezone_id'] ?? null; + if (isset($tz) && $timezone == $tz) { + $return = true; + break 2; + } + } + } + + return $return; + } + + /** + * Checks if a regular expression pattern is valid. + * + * @param string $regex regular expresion pattern + * + * @return bool TRUE if $regex is a valid regular expression + */ + public static function regex(string $regex): bool + { + set_error_handler(function () { }, E_WARNING); + $return = false !== preg_match($regex, ''); + restore_error_handler(); + + return $return; + } +} diff --git a/Chevereto-Chevere/src/VarDump/ConsoleVarDump.php b/Chevereto-Chevere/src/VarDump/ConsoleVarDump.php new file mode 100644 index 000000000..67095339d --- /dev/null +++ b/Chevereto-Chevere/src/VarDump/ConsoleVarDump.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\VarDump; + +use Chevere\VarDump\src\Wrapper; + +/** + * Analyze a variable and provide a CLI output string representation of its type and data. + */ +final class ConsoleVarDump extends VarDump +{ + public static function wrap(string $key, string $dump): ?string + { + $wrapper = new Wrapper($key, $dump); + $wrapper->useCli(); + + return $wrapper->toString(); + } +} diff --git a/Chevereto-Chevere/src/VarDump/Dumper.php b/Chevereto-Chevere/src/VarDump/Dumper.php new file mode 100644 index 000000000..b2f62c5fe --- /dev/null +++ b/Chevereto-Chevere/src/VarDump/Dumper.php @@ -0,0 +1,228 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Chevere\VarDump; + +use const Chevere\CLI; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Chevere\Path\Path; +use Chevere\Utility\Str; + +/** + * Dumps information about one or more variables. CLI/HTML aware. + */ +final class Dumper +{ + const BACKGROUND = '#132537'; + const BACKGROUND_SHADE = '#132537'; + const STYLE = 'font: 14px Consolas, monospace, sans-serif; line-height: 1.2; color: #ecf0f1; padding: 15px; margin: 10px 0; word-break: break-word; white-space: pre-wrap; background: '.self::BACKGROUND.'; display: block; text-align: left; border: none; border-radius: 4px;'; + + private $vars; + + /** @var int */ + private $numArgs; + + /** @var ConsoleOutputInterface */ + private $consoleOutput; + + /** @var string */ + private $output; + + /** @var string */ + private $outputHr; + + /** @var array */ + private $debugBacktrace; + + /** @var string */ + private $caller; + + /** @var string */ + private $callerFilepath; + + /** @var int */ + private $offset = 1; + + /** @var string */ + private $varDump; + + public function __construct(...$vars) + { + $this->varDump = VarDump::RUNTIME; + $this->vars = $vars; + $this->numArgs = func_num_args(); + if (0 == $this->numArgs) { + return; + } + $this->debugBacktrace = debug_backtrace(); + $this->caller = $this->debugBacktrace[0]; + $this->handleDebugBacktrace(); + $this->setCallerFilepath($this->debugBacktrace[0]['file']); + $this->handleSelfCaller(); + $this->output = null; + if ($this->varDump == ConsoleVarDump::class) { + $this->handleConsoleOutput(); + } else { + $this->handleHtmlOutput(); + } + $this->handleClass(); + $this->appendFunction($this->debugBacktrace[$this->offset]['function']); + $this->handleFile(); + $this->output .= "\n\n"; + $this->handleArgs(); + $this->output = trim($this->output); + $this->handleProccessOutput(); + } + + public static function dump(...$vars): void + { + new static(...$vars); + } + + /** + * Dumps information about one or more variables and die(). + */ + public static function dd(...$vars) + { + static::dump(...$vars); + die(1); + } + + private function handleDebugBacktrace(): void + { + while (isset($this->debugBacktrace[0]['file']) && __FILE__ == $this->debugBacktrace[0]['file']) { + $this->shiftDebugBacktrace(); + $this->caller = $this->debugBacktrace[0]; + } + } + + private function setCallerFilepath(string $filepath): void + { + $this->callerFilepath = Path::normalize($filepath); + } + + private function handleSelfCaller(): void + { + if (Str::endsWith('resources/functions/dump.php', $this->callerFilepath) && __CLASS__ == $this->debugBacktrace[0]['class'] && in_array($this->debugBacktrace[0]['function'], ['dump', 'dd'])) { + $this->shiftDebugBacktrace(); + } + } + + private function shiftDebugBacktrace(): void + { + array_shift($this->debugBacktrace); + } + + private function handleConsoleOutput(): void + { + $this->consoleOutput = new ConsoleOutput(); + $outputFormatter = new OutputFormatter(true); + $this->consoleOutput->setFormatter($outputFormatter); + $this->consoleOutput->getFormatter()->setStyle('block', new OutputFormatterStyle('red', 'black')); + $this->consoleOutput->getFormatter()->setStyle('dumper', new OutputFormatterStyle('blue', null, ['bold'])); + $this->consoleOutput->getFormatter()->setStyle('hr', new OutputFormatterStyle('blue')); + $this->outputHr = '
'.str_repeat('-', 60).''; + $this->consoleOutput->getFormatter()->setStyle('hr', new OutputFormatterStyle('blue', null)); + $maker = (isset($this->caller['class']) ? $this->caller['class'].$this->caller['type'] : null).$this->caller['function'].'()'; + $this->consoleOutput->writeln(['', ''.$maker.'', $this->outputHr]); + } + + private function handleHtmlOutput(): void + { + if (false === headers_sent()) { + $this->appendHtmlOpenBody(); + } + $this->appendStyle(); + } + + private function appendHtmlOpenBody(): void + { + $this->output .= ''; + } + + private function appendStyle(): void + { + $this->output .= '
';
+    }
+
+    private function handleClass(): void
+    {
+        if (isset($this->debugBacktrace[1]['class'])) {
+            $class = $this->debugBacktrace[$this->offset]['class'];
+            if (Str::startsWith('class@anonymous', $class)) {
+                $class = explode('0x', $class)[0];
+            }
+            $this->appendClass($class, $this->debugBacktrace[$this->offset]['type']);
+        }
+    }
+
+    private function appendClass(string $class, string $type): void
+    {
+        $this->output .= $this->varDump::wrap('_class', $class).$type;
+    }
+
+    private function appendFunction(string $function): void
+    {
+        $this->output .= $this->varDump::wrap('_function', $function.'()');
+    }
+
+    private function handleFile(): void
+    {
+        if (isset($this->debugBacktrace[0]['file'])) {
+            $this->appendFilepath($this->debugBacktrace[0]['file'], $this->debugBacktrace[0]['line']);
+        }
+    }
+
+    private function appendFilepath(string $file, int $line): void
+    {
+        $this->output .= "\n".$this->varDump::wrap('_file', Path::normalize($file).':'.$line);
+    }
+
+    private function handleArgs(): void
+    {
+        $pos = 1;
+        foreach ($this->vars as $value) {
+            $this->appendArg($pos, $value);
+            ++$pos;
+        }
+        // $this->output = trim($this->output, '\n');
+    }
+
+    private function appendArg(int $pos, $value): void
+    {
+        $this->output .= 'Arg#'.$pos.' '.$this->varDump::out($value, 0)."\n\n";
+    }
+
+    private function handleProccessOutput(): void
+    {
+        if (isset($this->consoleOutput)) {
+            $this->processConsoleOutput();
+        } else {
+            $this->processPrintOutput();
+        }
+    }
+
+    private function processConsoleOutput(): void
+    {
+        $this->consoleOutput->writeln($this->output, ConsoleOutput::OUTPUT_RAW);
+        isset($this->outputHr) ? $this->consoleOutput->writeln($this->outputHr) : null;
+    }
+
+    private function processPrintOutput(): void
+    {
+        echo $this->output;
+    }
+}
diff --git a/Chevereto-Chevere/src/VarDump/HtmlVarDump.php b/Chevereto-Chevere/src/VarDump/HtmlVarDump.php
new file mode 100644
index 000000000..e588a0eb0
--- /dev/null
+++ b/Chevereto-Chevere/src/VarDump/HtmlVarDump.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Chevere\VarDump;
+
+use Chevere\VarDump\src\Template;
+use Chevere\VarDump\src\Wrapper;
+
+/**
+ * Analyze a variable and provide a HTML output string representation of its type and data.
+ */
+final class HtmlVarDump extends VarDump
+{
+    protected function setPrefix(): void
+    {
+        $this->prefix = str_repeat(Template::HTML_INLINE_PREFIX, $this->indent);
+    }
+
+    protected function getEmphasis(string $string): string
+    {
+        return sprintf(Template::HTML_EMPHASIS, $string);
+    }
+
+    protected function filterChars(string $string): string
+    {
+        return htmlspecialchars($string);
+    }
+
+    public static function wrap(string $key, string $dump): ?string
+    {
+        $wrapper = new Wrapper($key, $dump);
+
+        return $wrapper->toString();
+    }
+}
diff --git a/Chevereto-Chevere/src/VarDump/PlainVarDump.php b/Chevereto-Chevere/src/VarDump/PlainVarDump.php
new file mode 100644
index 000000000..e5a93a49d
--- /dev/null
+++ b/Chevereto-Chevere/src/VarDump/PlainVarDump.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Chevere\VarDump;
+
+/**
+ * Analyze a variable and provide a plain text output string representation of its type and data.
+ */
+final class PlainVarDump extends VarDump
+{
+    public static function wrap(string $key, string $dump): ?string
+    {
+        return $dump;
+    }
+}
diff --git a/Chevereto-Chevere/src/VarDump/VarDump.php b/Chevereto-Chevere/src/VarDump/VarDump.php
new file mode 100644
index 000000000..23e90acff
--- /dev/null
+++ b/Chevereto-Chevere/src/VarDump/VarDump.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Chevere\VarDump;
+
+use const Chevere\CLI;
+use Chevere\VarDump\src\Wrapper;
+
+/**
+ * Analyze a variable and provide a CLI/HTML aware output string representation of its type and data.
+ */
+class VarDump extends VarDumpAbstract
+{
+    protected function setPrefix(): void
+    {
+        $this->prefix = str_repeat(' ', $this->indent);
+    }
+
+    protected function getEmphasis(string $string): string
+    {
+        return $string;
+    }
+
+    protected function filterChars(string $string): string
+    {
+        return $string;
+    }
+
+    public static function wrap(string $key, string $dump): ?string
+    {
+        $wrapper = new Wrapper($key, $dump);
+        if (CLI) {
+            $wrapper->useCli();
+        }
+
+        return $wrapper->toString();
+    }
+
+    public static function out($var, int $indent = null, array $dontDump = [], int $depth = 0): string
+    {
+        return (/* @scrutinizer ignore-call */new static(...func_get_args()))->toString();
+    }
+}
diff --git a/Chevereto-Chevere/src/VarDump/VarDumpAbstract.php b/Chevereto-Chevere/src/VarDump/VarDumpAbstract.php
new file mode 100644
index 000000000..e0af063cf
--- /dev/null
+++ b/Chevereto-Chevere/src/VarDump/VarDumpAbstract.php
@@ -0,0 +1,271 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Chevere\VarDump;
+
+use Throwable;
+use Reflector;
+use ReflectionProperty;
+use ReflectionObject;
+use const Chevere\CLI;
+use Chevere\Path\Path;
+use Chevere\Utility\Str;
+
+/**
+ * Analyze a variable and provide an output string representation of its type and data.
+ */
+abstract class VarDumpAbstract
+{
+    const RUNTIME = CLI ? ConsoleVarDump::class : HtmlVarDump::class;
+
+    const TYPE_STRING = 'string';
+    const TYPE_FLOAT = 'float';
+    const TYPE_INTEGER = 'integer';
+    const TYPE_BOOLEAN = 'boolean';
+    const TYPE_NULL = 'NULL';
+    const TYPE_OBJECT = 'object';
+    const TYPE_ARRAY = 'array';
+    const _FILE = '_file';
+    const _CLASS = '_class';
+    const _OPERATOR = '_operator';
+    const _FUNCTION = '_function';
+    const ANON_CLASS = 'class@anonymous';
+
+    const PROPERTIES_REFLECTION_MAP = [
+        'public' => ReflectionProperty::IS_PUBLIC,
+        'protected' => ReflectionProperty::IS_PROTECTED,
+        'private' => ReflectionProperty::IS_PRIVATE,
+        'static' => ReflectionProperty::IS_STATIC,
+    ];
+
+    /** @var string */
+    protected $output;
+
+    /** @var string */
+    protected $template;
+
+    /** @var string */
+    protected $className;
+
+    /** @var array */
+    protected $properties;
+
+    /** @var Reflector */
+    protected $reflectionObject;
+
+    protected $var;
+
+    /** @var mixed */
+    protected $expression;
+
+    /** @var int */
+    protected $indent;
+
+    /** @var array */
+    protected $dontDump;
+
+    /** @var int */
+    protected $depth;
+
+    /** @var mixed */
+    protected $val;
+
+    /** @var string */
+    protected $prefix;
+
+    /** @var string */
+    protected $type;
+
+    /** @var string */
+    protected $parentheses;
+
+    public function __construct($var, int $indent = null, array $dontDump = [], int $depth = 0)
+    {
+        ++$depth;
+        $this->var = $var;
+        // Maybe improve this to support any circular reference?
+        if (is_array($this->var)) {
+            $this->expression = array_merge([], $this->var);
+        } else {
+            $this->expression = $this->var;
+        }
+        $this->indent = $indent ?? 0;
+        $this->dontDump = $dontDump;
+        $this->depth = $depth;
+        $this->val = null;
+        $this->setPrefix();
+        $this->setType();
+        $this->handleType();
+        $this->setTemplate();
+        $this->handleParentheses();
+        $this->output = strtr($this->template, [
+            '%type' => static::wrap($this->type, $this->type),
+            '%val' => $this->val,
+            '%parentheses' => isset($this->parentheses) ? static::wrap(static::_OPERATOR, '(' . $this->parentheses . ')') : null,
+        ]);
+    }
+
+    abstract protected function setPrefix(): void;
+
+    abstract protected function getEmphasis(string $string): string;
+
+    abstract protected function filterChars(string $string): string;
+
+    abstract public static function wrap(string $key, string $dump): ?string;
+
+    protected function setType(): void
+    {
+        $this->type = gettype($this->expression);
+        if ('double' == $this->type) {
+            $this->type = static::TYPE_FLOAT;
+        }
+    }
+
+    protected function handleType(): void
+    {
+        switch ($this->type) {
+            case static::TYPE_BOOLEAN:
+                $this->val .= $this->expression ? 'TRUE' : 'FALSE';
+                break;
+            case static::TYPE_ARRAY:
+                ++$this->indent;
+                $this->processArray();
+                break;
+            case static::TYPE_OBJECT:
+                ++$this->indent;
+                $this->processObject();
+                break;
+            default:
+                $this->processDefault();
+                break;
+        }
+    }
+
+    protected function processObject(): void
+    {
+        $this->reflectionObject = new ReflectionObject($this->expression);
+        if (in_array($this->reflectionObject->getName(), $this->dontDump)) {
+            $this->val .= static::wrap(static::_OPERATOR, $this->getEmphasis($this->reflectionObject->getName()));
+
+            return;
+        }
+        $this->setProperties();
+        foreach ($this->properties as $k => $v) {
+            $this->processObjectProperty($k, $v);
+        }
+        $this->className = get_class($this->expression);
+        $this->handleNormalizeClassName();
+        $this->parentheses = $this->className;
+    }
+
+    protected function setProperties(): void
+    {
+        $this->properties = [];
+        foreach (static::PROPERTIES_REFLECTION_MAP as $k => $v) {
+            /** @scrutinizer ignore-call */
+            $v = $this->reflectionObject->getProperties($v);
+            foreach ($v as $kk => $vv) {
+                if (!isset($this->properties[$vv->getName()])) {
+                    $vv->setAccessible(true);
+                    $this->properties[$vv->getName()] = ['value' => $vv->getValue($this->expression)];
+                }
+                $this->properties[$vv->getName()]['visibility'][] = $k;
+            }
+        }
+    }
+
+    protected function processObjectProperty($key, $var): void
+    {
+        $visibility = implode(' ', $var['visibility'] ?? $this->properties['visibility']);
+        $operator = static::wrap(static::_OPERATOR, '->');
+        $this->val .= "\n" . $this->prefix . $this->getEmphasis($visibility) . ' ' . $this->filterChars($key) . " $operator ";
+        $aux = $var['value'];
+        if (is_object($aux) && property_exists($aux, $key)) {
+            try {
+                $r = new ReflectionObject($aux);
+                $p = $r->getProperty($key);
+                $p->setAccessible(true);
+                if ($aux == $p->getValue($aux)) {
+                    $this->val .= static::wrap(static::_OPERATOR, '(' . $this->getEmphasis('circular object reference') . ')');
+                }
+
+                return;
+            } catch (Throwable $e) {
+                return;
+            }
+        }
+        if ($this->depth < 4) {
+            $this->val .= (new static($aux, $this->indent, $this->dontDump, $this->depth))->toString();
+        } else {
+            $this->val .= static::wrap(static::_OPERATOR, '(' . $this->getEmphasis('max depth reached') . ')');
+        }
+    }
+
+    protected function processArray(): void
+    {
+        foreach ($this->expression as $k => $v) {
+            $operator = static::wrap(static::_OPERATOR, '=>');
+            $this->val .= "\n" . $this->prefix . ' ' . $this->filterChars((string) $k) . " $operator ";
+            $aux = $v;
+            $isCircularRef = is_array($aux) && isset($aux[$k]) && $aux == $aux[$k];
+            if ($isCircularRef) {
+                $this->val .= static::wrap(static::_OPERATOR, '(' . $this->getEmphasis('circular array reference') . ')');
+            } else {
+                $this->val .= (new static($aux, $this->indent, $this->dontDump))->toString();
+            }
+        }
+        $this->parentheses = 'size=' . count($this->expression);
+    }
+
+    protected function handleNormalizeClassName(): void
+    {
+        if (Str::startsWith(static::ANON_CLASS, $this->className)) {
+            $this->className = Path::normalize($this->className);
+        }
+    }
+
+    protected function processDefault(): void
+    {
+        $is_string = is_string($this->expression);
+        $is_numeric = is_numeric($this->expression);
+        if ($is_string || $is_numeric) {
+            $this->parentheses = 'length=' . strlen($is_numeric ? ((string) $this->expression) : $this->expression);
+            $this->val .= $this->filterChars(strval($this->expression));
+        }
+    }
+
+    protected function setTemplate(): void
+    {
+        switch ($this->type) {
+            case static::TYPE_ARRAY:
+            case static::TYPE_OBJECT:
+                $this->template = '%type %parentheses%val';
+                break;
+            default:
+                $this->template = '%type %val %parentheses';
+                break;
+        }
+    }
+
+    protected function handleParentheses(): void
+    {
+        if (isset($this->parentheses) && false !== strpos($this->parentheses, '=')) {
+            $this->parentheses = $this->getEmphasis($this->parentheses);
+        }
+    }
+
+    public function toString(): string
+    {
+        return $this->output ?? '';
+    }
+}
diff --git a/Chevereto-Chevere/src/VarDump/src/Pallete.php b/Chevereto-Chevere/src/VarDump/src/Pallete.php
new file mode 100644
index 000000000..93db6d47a
--- /dev/null
+++ b/Chevereto-Chevere/src/VarDump/src/Pallete.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Chevere\VarDump\src;
+
+use Chevere\VarDump\VarDump;
+
+abstract class Pallete
+{
+    /**
+     * Color palette used in HTML.
+     */
+    const PALETTE = [
+      VarDump::TYPE_STRING => '#e67e22', // orange
+      VarDump::TYPE_FLOAT => '#f1c40f', // yellow
+      VarDump::TYPE_INTEGER => '#f1c40f', // yellow
+      VarDump::TYPE_BOOLEAN => '#9b59b6', // purple
+      VarDump::TYPE_NULL => '#7f8c8d', // grey
+      VarDump::TYPE_OBJECT => '#e74c3c', // red
+      VarDump::TYPE_ARRAY => '#2ecc71', // green
+      VarDump::_FILE => null,
+      VarDump::_CLASS => '#3498db', // blue
+      VarDump::_OPERATOR => '#7f8c8d', // grey
+      VarDump::_FUNCTION => '#9b59b6', // purple
+    ];
+
+    /**
+     * Color palette used in CLI.
+     */
+    const CONSOLE = [
+      VarDump::TYPE_STRING => 'color_136', // yellow
+      VarDump::TYPE_FLOAT => 'color_136', // yellow
+      VarDump::TYPE_INTEGER => 'color_136', // yellow
+      VarDump::TYPE_BOOLEAN => 'color_127', // purple
+      VarDump::TYPE_NULL => 'color_245', // grey
+      VarDump::TYPE_OBJECT => 'color_167', // red
+      VarDump::TYPE_ARRAY => 'color_41', // green
+      VarDump::_FILE => null,
+      VarDump::_CLASS => 'color_147', // blue
+      VarDump::_OPERATOR => 'color_245', // grey
+      VarDump::_FUNCTION => 'color_127', // purple
+    ];
+}
diff --git a/Chevereto-Chevere/src/VarDump/src/Template.php b/Chevereto-Chevere/src/VarDump/src/Template.php
new file mode 100644
index 000000000..3f5e744fb
--- /dev/null
+++ b/Chevereto-Chevere/src/VarDump/src/Template.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Chevere\VarDump\src;
+
+/**
+ * The template strings used by VarDump*.
+ */
+abstract class Template
+{
+    const HTML_INLINE_PREFIX = '   ';
+    const HTML_EMPHASIS = '%s';
+}
diff --git a/Chevereto-Chevere/src/VarDump/src/Wrapper.php b/Chevereto-Chevere/src/VarDump/src/Wrapper.php
new file mode 100644
index 000000000..b6df83185
--- /dev/null
+++ b/Chevereto-Chevere/src/VarDump/src/Wrapper.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Chevere\VarDump\src;
+
+use JakubOnderka\PhpConsoleColor\ConsoleColor;
+
+/**
+ * Wraps dump data into colored output for CLI and HTML.
+ */
+class Wrapper
+{
+    /** @var string */
+    public $key;
+
+    /** @var string */
+    public $dump;
+
+    /** @var ConsoleColor */
+    public $consoleColor;
+
+    /** @var bool */
+    protected $isCli = false;
+
+    /**
+     * @param string $key color palette key
+     */
+    public function __construct(string $key, string $dump)
+    {
+        $this->key = $key;
+        $this->dump = $dump;
+    }
+
+    public function useCli()
+    {
+        $this->consoleColor = new ConsoleColor();
+        $this->isCli = true;
+    }
+
+    /**
+     * @return string color
+     */
+    public function toString(): string
+    {
+        return $this->wrap() ?? '';
+    }
+
+    protected function getCliColor(string $key): ?string
+    {
+        return Pallete::CONSOLE[$key] ?? null;
+    }
+
+    protected function getHtmlColor(string $key): ?string
+    {
+        return Pallete::PALETTE[$key] ?? null;
+    }
+
+    protected function wrapCli(): string
+    {
+        if ($color = $this->getCliColor($this->key)) {
+            return $this->consoleColor->apply($color, $this->dump);
+        }
+
+        return $this->dump;
+    }
+
+    protected function wrapHtml()
+    {
+        if ($color = $this->getHtmlColor($this->key)) {
+            return ''.$this->dump.'';
+        }
+
+        return $this->dump;
+    }
+
+    /**
+     * Wrap dump data.
+     *
+     * @return string wrapped dump data
+     */
+    public function wrap(): string
+    {
+        return $this->isCli ? $this->wrapCli() : $this->wrapHtml();
+    }
+}
diff --git a/Chevereto-Chevere/tests/Api/MakerTest.php b/Chevereto-Chevere/tests/Api/MakerTest.php
new file mode 100644
index 000000000..920afee6a
--- /dev/null
+++ b/Chevereto-Chevere/tests/Api/MakerTest.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+// namespace Tests\Api;
+
+// use Throwable;
+// use Chevere\Api\Maker;
+// // use Chevere\Router\Router;
+// use Chevere\Contracts\Api\MakerContract;
+// use PHPUnit\Framework\TestCase;
+
+// final class MakerTest extends TestCase
+// {
+//     /** @var MakerContract */
+//     protected $maker;
+
+//     public function setUp(): void
+//     {
+//         $this->maker = new Maker(new Router());
+//     }
+
+//     public function tearDown(): void
+//     {
+//         unset($this->maker);
+//     }
+
+//     /**
+//      * @doesNotPerformAssertions
+//      */
+//     public function testCanRegisterApiPath(): void
+//     {
+//         $this->maker->register('src/Api/');
+//     }
+
+//     public function testCannotRegisterInvalidPath(): void
+//     {
+//         $this->expectException(Throwable::class);
+//         $this->maker->register('/dev/null');
+//     }
+// }
diff --git a/Chevereto-Chevere/utils/phpcheck.php b/Chevereto-Chevere/utils/phpcheck.php
new file mode 100644
index 000000000..7fe6ab4ea
--- /dev/null
+++ b/Chevereto-Chevere/utils/phpcheck.php
@@ -0,0 +1,13 @@
+ 'America/Bogota',
+    Config::DEBUG => 1,
+    Config::TIMEZONE => 'UTC',
+    // Config::ERROR_HANDLER => null,
+    // Config::EXCEPTION_HANDLER => null,
+    Config::URI_SCHEME => 'https',
+];
diff --git a/app/console b/app/console
new file mode 100644
index 000000000..aff55f599
--- /dev/null
+++ b/app/console
@@ -0,0 +1,7 @@
+#!/usr/bin/env php
+
+ * You can also use: php index.php 
+ */
+require_once dirname(__DIR__) . '/index.php';
diff --git a/app/hacks.php b/app/hacks.php
new file mode 100644
index 000000000..968430cde
--- /dev/null
+++ b/app/hacks.php
@@ -0,0 +1,5 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Chevere\App\Loader;
+
+(new Loader())->run();
diff --git a/app/parameters.php b/app/parameters.php
new file mode 100644
index 000000000..b236692ec
--- /dev/null
+++ b/app/parameters.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Chevere\App\Parameters;
+
+return [
+  Parameters::API => 'src/Api/',
+  Parameters::ROUTES => [
+    'routes:dashboard',
+    'routes:web',
+  ],
+];
diff --git a/app/routes/dashboard.php b/app/routes/dashboard.php
new file mode 100644
index 000000000..1819f5352
--- /dev/null
+++ b/app/routes/dashboard.php
@@ -0,0 +1,10 @@
+setName('homepageHtml'),
+  'index' => (new Route('/', Controllers\Index::class))
+    ->setName('homepage'),
+  // ->addMiddleware(Middlewares\RoleAdmin::class)
+  // ->addMiddleware(Middlewares\RoleBanned::class),
+  (new Route('/cache/{llave?}-{cert}-{user?}'))
+    ->setWhere('llave', '[0-9]+')
+    ->setMethod(new Method('GET', Controllers\Cache::class))
+    ->setMethod(new Method('POST', Controllers\Cache::class))
+    ->setName('cache'),
+];
diff --git a/app/settings.php b/app/settings.php
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/src/Api/Users/DELETE.php b/app/src/Api/Users/DELETE.php
new file mode 100644
index 000000000..3f6536777
--- /dev/null
+++ b/app/src/Api/Users/DELETE.php
@@ -0,0 +1,23 @@
+invoke('@:GET', $user);
+        // $this->source = 'deez';
+        // // $that is "this"
+        // $this->hookable('deleteUser', function ($that) use ($user) {
+        //     $that->private .= ' - MC HAMMER';
+        //     $that->source .= ' nuuuuts ';
+        // });
+    }
+}
diff --git a/app/src/Api/Users/Friends/Relationship.php b/app/src/Api/Users/Friends/Relationship.php
new file mode 100644
index 000000000..78044108b
--- /dev/null
+++ b/app/src/Api/Users/Friends/Relationship.php
@@ -0,0 +1,13 @@
+user);
+    }
+}
diff --git a/app/src/Api/Users/PATCH.php b/app/src/Api/Users/PATCH.php
new file mode 100644
index 000000000..0b9f6932e
--- /dev/null
+++ b/app/src/Api/Users/PATCH.php
@@ -0,0 +1,16 @@
+ [
+            'description' => 'User email.',
+        ],
+    ];
+}
diff --git a/app/src/Api/Users/Resource.php b/app/src/Api/Users/Resource.php
new file mode 100644
index 000000000..3f52bb4c1
--- /dev/null
+++ b/app/src/Api/Users/Resource.php
@@ -0,0 +1,18 @@
+ className] */
+    protected static $resources = [
+        'user' => User::class,
+    ];
+    /** @var User The user entity resource */
+    protected $user;
+}
diff --git a/app/src/Api/Users/_GET.php b/app/src/Api/Users/_GET.php
new file mode 100644
index 000000000..b434f5294
--- /dev/null
+++ b/app/src/Api/Users/_GET.php
@@ -0,0 +1,12 @@
+ [
+            'description' => 'Username.',
+        ],
+        'email' => [
+            'description' => 'User email.',
+        ],
+    ];
+}
diff --git a/app/src/Controller.php b/app/src/Controller.php
new file mode 100644
index 000000000..c8d6754f8
--- /dev/null
+++ b/app/src/Controller.php
@@ -0,0 +1,13 @@
+ User::class,
+    ];
+
+    public function __invoke(string $llave, string $user, string $cert)
+    {
+        // $this->app->response()->setContent('eee');
+        // dd(func_get_args(), ['llave' => $llave, 'user' => $user, 'cert' => $cert]);
+    }
+
+    public function render(): ?string
+    {
+        $response = $this->response;
+
+        // return var_export($response->getData(), true);
+    }
+}
diff --git a/app/src/Controllers/Dashboard.php b/app/src/Controllers/Dashboard.php
new file mode 100644
index 000000000..20391a5cd
--- /dev/null
+++ b/app/src/Controllers/Dashboard.php
@@ -0,0 +1,14 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Controllers;
+
+use Chevere\Contracts\Render\RenderContract;
+use Chevere\Controller\Controller;
+use Chevere\JsonApi\Data;
+
+class Home extends Controller implements RenderContract
+{
+    public function __invoke()
+    {
+        $api = new Data('hello', 'World!');
+        $this->document->appendData($api);
+    }
+
+    public function render()
+    {
+        echo $this->document->toString();
+    }
+}
diff --git a/app/src/Controllers/Index.php b/app/src/Controllers/Index.php
new file mode 100644
index 000000000..077dad6e2
--- /dev/null
+++ b/app/src/Controllers/Index.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Controllers;
+
+use Chevere\Controller\Controller;
+use Chevere\JsonApi\Data;
+
+class Index extends Controller
+{
+    public function __invoke()
+    {
+        $api = new Data('info', 'api');
+        $api->addAttribute('entry', 'HTTP GET /api');
+        $api->addAttribute('description', 'Retrieves the exposed API.');
+
+        $cli = new Data('info', 'cli');
+        $cli->addAttribute('entry', 'php app/console list');
+        $cli->addAttribute('description', 'Retrieves the console command list.');
+
+        $this->document->appendData($api, $cli);
+        // dd($this->document);
+    }
+}
diff --git a/app/src/Controllers/PostComments.php b/app/src/Controllers/PostComments.php
new file mode 100644
index 000000000..a6f5804ee
--- /dev/null
+++ b/app/src/Controllers/PostComments.php
@@ -0,0 +1,23 @@
+extra();
+    }
+
+    public function extra()
+    {
+        echo ' extra output ';
+    }
+}
diff --git a/app/src/Middlewares/RoleAdmin.php b/app/src/Middlewares/RoleAdmin.php
new file mode 100644
index 000000000..911d82cc3
--- /dev/null
+++ b/app/src/Middlewares/RoleAdmin.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Middlewares;
+
+use Chevere\Interfaces\HandlerInterface;
+use Chevere\Interfaces\MiddlewareInterface;
+use Chevere\Http\Request\RequestException;
+
+class RoleAdmin implements MiddlewareInterface
+{
+    public function __construct(HandlerInterface $handler)
+    {
+        $userRole = 'user';
+        if ('admin' != $userRole) {
+            return $handler->stop(
+                new RequestException(401, sprintf('User must have the admin role, %s role found', $userRole))
+            );
+        }
+        return $handler->handle();
+    }
+};
diff --git a/app/src/Middlewares/RoleBanned.php b/app/src/Middlewares/RoleBanned.php
new file mode 100644
index 000000000..6a6b0badf
--- /dev/null
+++ b/app/src/Middlewares/RoleBanned.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace App\Middlewares;
+
+use Chevere\Contracts\App\AppContract;
+use Chevere\Interfaces\HandlerInterface;
+use Chevere\Interfaces\MiddlewareInterface;
+
+class RoleBanned implements MiddlewareInterface
+{
+    public function __invoke(AppContract $app, HandlerInterface $handler)
+    {
+        \dump(__FILE__);
+        // $userRole = 'user';
+        // if ('banned' == $userRole) {
+        //     return $handler->stop($app);
+        // }
+        return $handler->process($app);
+    }
+};
diff --git a/app/src/User.php b/app/src/User.php
new file mode 100644
index 000000000..1b88617a8
--- /dev/null
+++ b/app/src/User.php
@@ -0,0 +1,66 @@
+hasConstructArgument = true;
+            // DB HANDLE
+        } else {
+            $this->hasConstructArgument = false;
+        }
+    }
+
+    /**
+     * @param string Username
+     *
+     * @see FromString, $stringDescription, $stringRegex
+     */
+    public function createFromString(string $string): CreateFromString
+    {
+        $this->assertNoConstructArgument();
+        $this->assertFromString($string);
+        $this->fromStringArgument = $string;
+
+        return $this;
+    }
+
+    /**
+     * Throws a LogicException if the class was constructed with an argument.
+     */
+    protected function assertNoConstructArgument(): void
+    {
+        if ($this->hasConstructArgument) {
+            throw new LogicException(
+                (new Message('An instance of %s has been already created (WHERE?).'))
+                    ->code('%s', __CLASS__)
+                    ->toString()
+            );
+        }
+    }
+}
diff --git a/app/translations/traducciones po.txt b/app/translations/traducciones po.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/composer.json b/composer.json
new file mode 100644
index 000000000..cd80f558c
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,31 @@
+{
+  "name": "chevereto/app",
+  "description": "Chevereto app",
+  "type": "library",
+  "license": "MIT",
+  "authors": [{
+    "name": "Rodolfo Berrios",
+    "email": "inbox@rodolfoberrios.com"
+  }],
+  "repositories": [{
+    "type": "path",
+    "url": "./Chevereto-Chevere",
+    "options": {
+      "symlink": true
+    }
+  }],
+  "autoload": {
+    "psr-4": {
+      "App\\": "app/src/"
+    }
+  },
+  "require": {
+    "chevereto/chevere": "@dev"
+  },
+  "require-dev": {
+    "phpunit/phpunit": "^8"
+  },
+  "config": {
+    "optimize-autoloader": true
+  }
+}
\ No newline at end of file
diff --git a/content/APP GENERATED CONTENT.txt b/content/APP GENERATED CONTENT.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/content/images/system/fondos, covers, etc.txt b/content/images/system/fondos, covers, etc.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/content/images/users/Contenido de usuario bajo FOLDER ID.txt b/content/images/users/Contenido de usuario bajo FOLDER ID.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/extend/user/USER CUSTOMIZATION.txt b/extend/user/USER CUSTOMIZATION.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/index.php b/index.php
new file mode 100644
index 000000000..02a02d8f4
--- /dev/null
+++ b/index.php
@@ -0,0 +1,15 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+define('BOOT_TIMESTAMP', microtime(true));
+require_once 'app/bootstrap.php';
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 000000000..61037d48f
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,13 @@
+parameters:
+    ignoreErrors:
+        - '#Constant MIN_PHP_VERSION not found#'
+        - '#Call to an undefined method Chevereto\\Core\\Message::.*#'
+        - '#Call to method .* on an unknown class Chevereto\\Core\\Utils\\Message#'
+        - '#Constant PATH not found#'
+        - '#Constant ROOT_PATH not found#'
+        - '#Constant Chevereto\\Core\\App\\.* not found#'
+        - '#Constant CORE_NS_HANDLE not found#'
+        - '#Constant TIME_BOOTSTRAP not found#'
+        - '#Parameter * of class ReflectionFunction constructor expects Closure|string, callable given#'
+        - '#Access to an undefined property DateInterval::\$w#'
+