├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .kodiak.toml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── generate-facades.php ├── book.json ├── composer.json ├── composer.lock ├── docs ├── .gitbook │ └── assets │ │ └── siler.png ├── README.md ├── SUMMARY.md ├── concepts-and-philosophy.md ├── functional.md ├── graphql.md ├── graphql │ ├── README.md │ └── annotations.md ├── psrs-and-middlewares-pipelines.md ├── routing.md ├── swoole.md ├── twig-templating.md └── web-servers.md ├── examples ├── README.md ├── functional │ └── functional.php ├── graphql-annotations │ ├── .gitignore │ ├── .graphqlconfig │ ├── composer.json │ ├── composer.lock │ ├── index.php │ ├── schema.php │ ├── src │ │ ├── Bar.php │ │ ├── Foo.php │ │ ├── FooBar.php │ │ ├── HelloWorld.php │ │ ├── ITodo.php │ │ ├── Mutation.php │ │ ├── Query.php │ │ ├── Subscription.php │ │ ├── Todo.php │ │ ├── TodoStatus.php │ │ ├── TupleInput.php │ │ └── Upper.php │ └── subscriptions.php ├── graphql │ ├── .gitignore │ ├── .graphqlconfig │ ├── README.md │ ├── directives.php │ ├── docker-compose.yml │ ├── filters.php │ ├── resolvers.php │ ├── sapi.php │ ├── schema.graphql │ ├── swoole.php │ └── uploads │ │ └── .gitignore ├── grpc │ ├── .dockerignore │ ├── Dockerfile │ ├── README.md │ ├── clients │ │ ├── dart │ │ │ ├── .gitignore │ │ │ ├── bin │ │ │ │ └── client.dart │ │ │ ├── lib │ │ │ │ └── src │ │ │ │ │ └── generated │ │ │ │ │ ├── helloworld.pb.dart │ │ │ │ │ ├── helloworld.pbenum.dart │ │ │ │ │ ├── helloworld.pbgrpc.dart │ │ │ │ │ └── helloworld.pbjson.dart │ │ │ ├── pubspec.lock │ │ │ └── pubspec.yaml │ │ ├── go │ │ │ ├── .gitignore │ │ │ ├── helloworld.pb.go │ │ │ └── main.go │ │ ├── node │ │ │ ├── .gitignore │ │ │ ├── client.js │ │ │ ├── helloworld_grpc_pb.js │ │ │ ├── helloworld_pb.js │ │ │ ├── package-lock.json │ │ │ └── package.json │ │ └── php │ │ │ ├── .gitignore │ │ │ ├── bin │ │ │ └── client.php │ │ │ ├── composer.json │ │ │ └── src │ │ │ ├── GPBMetadata │ │ │ └── Helloworld.php │ │ │ └── Helloworld │ │ │ ├── GreeterClient.php │ │ │ ├── HelloReply.php │ │ │ └── HelloRequest.php │ ├── proto_gen.sh │ ├── protos │ │ └── helloworld.proto │ └── server.php ├── hello-world │ ├── README.md │ ├── hello-world.phtml │ └── index.php ├── laminas │ ├── hello-world │ │ └── index.php │ ├── index.php │ └── routes │ │ └── index.php ├── mail │ ├── .env.example │ ├── .gitignore │ └── swiftmailer.php ├── monolog │ ├── index.php │ └── siler.log ├── psr7-diactoros │ ├── index.php │ └── template.twig ├── psr7-route │ └── index.php ├── route-any │ └── index.php ├── route-files │ ├── .models │ ├── README.md │ ├── controllers │ │ ├── about.get.php │ │ ├── contact.get.php │ │ ├── contact.post.php │ │ └── index.get.php │ ├── index.php │ └── views │ │ ├── about.twig │ │ ├── contact.twig │ │ ├── home.twig │ │ └── layout.twig ├── route-not-found │ ├── books │ │ └── index.php │ └── index.php ├── swoole-chat │ ├── docker-compose.yml │ ├── index.html │ └── server.php ├── swoole │ ├── README.md │ ├── api │ │ └── todos.php │ ├── docker-compose.yml │ ├── index.php │ ├── pages │ │ ├── _layout.twig │ │ ├── home.php │ │ └── home.twig │ └── public │ │ └── assets │ │ ├── scripts.js │ │ └── styles.css └── twig │ ├── home.twig │ └── index.php ├── media ├── README.md ├── logo.psd ├── siler.png └── siler_avatar.jpg ├── phpcs.xml.dist ├── phpunit.xml.dist ├── psalm.xml.dist ├── siler.png ├── src ├── Config │ └── Config.php ├── Container │ └── Container.php ├── Diactoros │ └── Diactoros.php ├── Dotenv │ └── Dotenv.php ├── Encoder │ └── Json.php ├── Env │ └── Env.php ├── File │ └── File.php ├── Functional │ ├── Functional.php │ └── Monad │ │ ├── Identity.php │ │ ├── Maybe.php │ │ └── Monad.php ├── GraphQL │ ├── Annotation │ │ ├── Args.php │ │ ├── Directive.php │ │ ├── EnumType.php │ │ ├── EnumVal.php │ │ ├── Field.php │ │ ├── InputType.php │ │ ├── InterfaceType.php │ │ ├── ObjectType.php │ │ └── UnionType.php │ ├── BuildSchema.php │ ├── DateScalar.php │ ├── DateTimeScalar.php │ ├── Deannotator.php │ ├── GraphQL.php │ ├── GraphiQL.php │ ├── Request.php │ ├── SubscriptionsConnection.php │ └── SubscriptionsManager.php ├── Grpc │ ├── Grpc.php │ └── Parser.php ├── Http │ ├── Http.php │ ├── Request.php │ └── Response.php ├── HttpHandlerRunner │ └── HttpHandlerRunner.php ├── Mail │ └── SwiftMailer.php ├── Monolog │ ├── Loggers.php │ └── Monolog.php ├── Prelude │ ├── Arr.php │ ├── Collection.php │ ├── Dispatcher.php │ ├── Enum.php │ ├── FromArray.php │ ├── FromArrayInterface.php │ ├── IO.php │ ├── Klass.php │ ├── Obj.php │ ├── Patch.php │ ├── PatchInterface.php │ ├── Prelude.php │ ├── Str.php │ ├── ToArray.php │ ├── ToArrayInterface.php │ └── Tuple.php ├── Ratchet │ ├── GraphQLSubscriptionsConnection.php │ ├── GraphQLSubscriptionsServer.php │ └── Ratchet.php ├── Route │ └── Route.php ├── Siler.php ├── Stratigility │ ├── RequestHandlerDecorator.php │ └── Stratigility.php ├── Swoole │ ├── GraphQLSubscriptionsConnection.php │ └── Swoole.php ├── Twig │ └── Twig.php └── facades.php └── tests ├── Integration ├── ComposabilityTest.php ├── FizzbuzzTest.php └── RoutingTest.php ├── Unit ├── Config │ └── ConfigTest.php ├── Container │ └── ContainerTest.php ├── Diactoros │ └── DiactorosTest.php ├── Dotenv │ └── DotenvTest.php ├── Encoder │ └── JsonTest.php ├── Env │ └── EnvTest.php ├── File │ └── FileTest.php ├── Functional │ ├── FunctionalTest.php │ └── MonadTest.php ├── GraphQL │ ├── Annotated │ │ ├── Bar.php │ │ ├── Enum.php │ │ ├── Foo.php │ │ ├── FooBar.php │ │ ├── IFoo.php │ │ ├── Input.php │ │ ├── ListOfException.php │ │ ├── Mutation.php │ │ ├── MyDirective.php │ │ └── Query.php │ ├── AnnotatedTest.php │ ├── BuildSchemaTest.php │ ├── DateTimeScalarTest.php │ ├── GraphQLResolverTest.php │ ├── GraphQLTest.php │ └── SubscriptionsManagerTest.php ├── Http │ ├── HttpTest.php │ ├── RequestTest.php │ └── ResponseTest.php ├── HttpHandlerRunner │ └── HttpHandlerRunnerTest.php ├── Mail │ └── SwiftMailerTest.php ├── Monolog │ └── MonologTest.php ├── Prelude │ ├── ArrTest.php │ ├── CollectionTest.php │ ├── DispatcherTest.php │ ├── EnumTest.php │ ├── FromToArrayTest.php │ ├── IOTest.php │ ├── ObjTest.php │ ├── PatchFromToFixture.php │ ├── StrTest.php │ ├── Test.php │ ├── TestEnum.php │ └── TupleTest.php ├── Ratchet │ ├── GraphQLSubscriptionsConnectionTest.php │ ├── GraphQLSubscriptionsServerTest.php │ └── RatchetTest.php ├── Route │ ├── RouteClass.php │ ├── RouteClassNameTest.php │ ├── RouteFacadeTest.php │ ├── RouteFileTest.php │ ├── RouteFileWithPrefixTest.php │ ├── RoutePsr7Test.php │ ├── RouteResourceTest.php │ ├── RouteStaticMethodTest.php │ ├── RouteTest.php │ ├── RouteUtf8Test.php │ └── SwooleHttpRequestMock.php ├── SilerTest.php ├── Stratigility │ ├── RequestHandlerDecoratorTest.php │ └── StratigilityTest.php ├── Swoole │ └── SwooleTest.php └── Twig │ └── TwigTest.php └── fixtures ├── .env ├── TestEvent.php ├── callable_require.php ├── concat ├── bar.txt └── foo.txt ├── config ├── test.ext ├── test.ini ├── test.php └── yaml │ └── test.yml ├── foo.php ├── graphql_error.json ├── graphql_input.json ├── php_input.json ├── php_input.txt ├── resources ├── create.php ├── destroy.php ├── edit.php ├── index.php ├── show.php ├── slug │ └── edit.php ├── store.php └── update.php ├── route_files ├── about │ └── index.get.php ├── contact.get.php ├── contact.post.php ├── foo.$id.get.php ├── foo.@id.get.php ├── foo.{id}.get.php └── index.get.php ├── schema.graphql ├── static.twig ├── template.twig ├── test.csv └── to_be_required.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 2 10 | 11 | [*.php] 12 | indent_size = 4 13 | 14 | [{composer.json,*.neon}] 15 | indent_size = 4 16 | 17 | [{*.txt,*.json}] 18 | insert_final_newline = false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [leocavalcante] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "08:00" 8 | open-pull-requests-limit: 10 9 | target-branch: main 10 | labels: 11 | - automerge 12 | versioning-strategy: increase 13 | allow: 14 | - dependency-type: direct 15 | - dependency-type: indirect 16 | ignore: 17 | - dependency-name: vimeo/psalm 18 | versions: 19 | - 4.7.0 20 | - dependency-name: symfony/polyfill-intl-grapheme 21 | versions: 22 | - 1.22.1 23 | - dependency-name: symfony/polyfill-php80 24 | versions: 25 | - 1.22.1 26 | - dependency-name: symfony/polyfill-ctype 27 | versions: 28 | - 1.22.1 29 | - dependency-name: symfony/polyfill-php73 30 | versions: 31 | - 1.22.1 32 | - dependency-name: symfony/polyfill-intl-normalizer 33 | versions: 34 | - 1.22.1 35 | - dependency-name: symfony/string 36 | versions: 37 | - 5.2.3 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | php-versions: ['7.4', '8.0'] 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | 16 | - name: Setup 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php-versions }} 20 | coverage: xdebug 21 | 22 | - name: Validate 23 | run: composer validate 24 | 25 | - name: Cache dir 26 | id: composer-cache 27 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 28 | 29 | - name: Cache 30 | uses: actions/cache@v2 31 | with: 32 | path: ${{ steps.composer-cache.outputs.dir }} 33 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: ${{ runner.os }}-composer- 35 | 36 | - name: Install 37 | run: composer install --prefer-dist --no-progress --no-suggest 38 | 39 | - name: Test 40 | run: composer ci 41 | 42 | - name: Coverage 43 | uses: codecov/codecov-action@v1 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.cache 2 | *.log 3 | /*.phar 4 | /.idea/ 5 | /coverage-* 6 | /issues/ 7 | /phpcs.xml 8 | /phpunit.xml 9 | /psalm.xml 10 | /vendor/ 11 | -------------------------------------------------------------------------------- /.kodiak.toml: -------------------------------------------------------------------------------- 1 | # .kodiak.toml 2 | # Minimal config. version is the only required field. 3 | version = 1 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2021 Leo Cavalcante 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.5.3 | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Mail lc at leocavalcante.com. 24 hours SLA. 12 | -------------------------------------------------------------------------------- /bin/generate-facades.php: -------------------------------------------------------------------------------- 1 | - 3 | Siler is a set of general purpose high-level abstractions aiming an API for 4 | declarative programming in PHP. 5 | --- 6 | 7 | # Siler 8 | 9 | > Simplicity is the ultimate sophistication. – Leonardo Da Vinci 10 | 11 | You can use it within any framework or standalone, as a micro-framework: 12 | 13 | ```bash 14 | composer require leocavalcante/siler 15 | ``` 16 | 17 | ## A Hello World example 18 | 19 | ```php 20 | use Siler\Functional as λ; // Just to be cool, don't use non-ASCII identifiers ;) 21 | use Siler\Route; 22 | 23 | Route\get('/', λ\puts('Hello World')); 24 | ``` 25 | 26 | This outputs "Hello World" when the file is reached via HTTP using the GET method and an URI path that matches "/". **Got the idea, right?** 27 | 28 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Siler](README.md) 4 | * [Routing](routing.md) 5 | * [PSRs & Middleware Pipeline](psrs-and-middlewares-pipelines.md) 6 | * [Twig Templating](twig-templating.md) 7 | * [GraphQL](graphql/README.md) 8 | * [@Annotations](graphql/annotations.md) 9 | * [Web Servers](web-servers.md) 10 | * [Siler ❤️ Swoole](swoole.md) 11 | * [λ Functional](functional.md) 12 | * [Concepts & Philosophy](concepts-and-philosophy.md) 13 | 14 | -------------------------------------------------------------------------------- /docs/twig-templating.md: -------------------------------------------------------------------------------- 1 | # Twig Templating 2 | 3 | ``` 4 | composer require twig/twig 5 | ``` 6 | 7 | {% hint style="info" %} 8 | Siler doesn't have direct dependencies, to stay fit, it favors peer dependencies, which means you have to explicitly declare a `twig` dependency in your project in order to use it. 9 | {% endhint %} 10 | 11 | Siler will internally handle the `Twig_Environment` instance. 12 | 13 | ```php 14 | use Siler\Twig; 15 | 16 | Twig\init('path/to/templates'); 17 | ``` 18 | 19 | Actually it is also returned at `init` function call, so you call add Twig plugins, filters and functions, for example, adding `Siler\Http\url` into Twig's Environment to later reference static assets on the public folder: 20 | 21 | ```php 22 | Twig\init('path/to/templates') 23 | ->addFunction(new Twig_SimpleFunction('url', 'Siler\Http\url')); 24 | ``` 25 | 26 | At initialization, you can also provide a path to templates cache as second argument and if you want to let Twig debug as third argument \(defaults to `false`\): 27 | 28 | ```php 29 | $shouldTwigDebug = true; 30 | Twig\init('path/to/templates', 'path/to/templates/cache', $shouldTwigDebug); 31 | ``` 32 | 33 | To render a template, simply call `render` at Twig namespace: 34 | 35 | ```php 36 | echo Twig\render('pages/home.twig'); 37 | ``` 38 | 39 | An passing parameters can be done by the second argument: 40 | 41 | ```php 42 | $data = ['message' => 'Hello World']; 43 | echo Twig\render('pages/home.twig', $data); 44 | ``` 45 | 46 | Something that can be confusing using Siler is that some function does outputs and other doesn't, like `Twig\render`. So remember that `Twig\render` will only return the rendered template within its given data and you should explicit output or let `Response` do it: 47 | 48 | ```php 49 | $html = Twig\render('pages/home.twig'); 50 | Response\html($html); 51 | ``` 52 | 53 | Also, remember that **you can always bring you own template engine to the playground without any bridging stuff** or use PHP itself on your views. 54 | 55 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Siler Examples 2 | 3 | See https://github.com/siler-examples for more. -------------------------------------------------------------------------------- /examples/functional/functional.php: -------------------------------------------------------------------------------- 1 | trim($s)), 9 | λ\lfilter(λ\not(λ\equal('baz'))), 10 | λ\non_empty, 11 | λ\ljoin(',') 12 | ]); 13 | 14 | echo $pipeline(['foo', ' ', 'bar', 'baz']); // foo,bar 15 | -------------------------------------------------------------------------------- /examples/graphql-annotations/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /examples/graphql-annotations/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Annotated GraphQL Schema", 3 | "extensions": { 4 | "endpoints": { 5 | "Default GraphQL Endpoint": { 6 | "url": "http://localhost:8080/graphql" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/graphql-annotations/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=7.4" 4 | }, 5 | "autoload": { 6 | "psr-4": { 7 | "Siler\\Example\\GraphQL\\Annotation\\": "src/" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /examples/graphql-annotations/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "03b47b7576525da6cbb4b64a32d9eaf0", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": ">=7.4" 17 | }, 18 | "platform-dev": [], 19 | "plugin-api-version": "1.1.0" 20 | } 21 | -------------------------------------------------------------------------------- /examples/graphql-annotations/index.php: -------------------------------------------------------------------------------- 1 | init($schema)); 17 | -------------------------------------------------------------------------------- /examples/graphql-annotations/schema.php: -------------------------------------------------------------------------------- 1 | x + $input->y; 26 | } 27 | 28 | /** 29 | * @Field(description="Sums of tuples", listOf="int") 30 | * @Args({ 31 | * @Field(name="inputs", listOf=TupleInput::class, nullableList=true) 32 | * }) 33 | * @return array 34 | */ 35 | public static function sums($_, array $args): array 36 | { 37 | return array_map(static function (array $input) { 38 | $input = TupleInput::fromArray($input); 39 | return $input->x + $input->y; 40 | }, array_get_arr($args, 'inputs')); 41 | } 42 | 43 | /** 44 | * @Field() 45 | * @Args({@Field(name="message", type="string")}) 46 | */ 47 | public static function ekko($_, array $args): string 48 | { 49 | publish('ekko', $args['message']); 50 | return $args['message']; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/graphql-annotations/src/Query.php: -------------------------------------------------------------------------------- 1 | 50 ? new Foo() : new Bar(); 37 | } 38 | 39 | /** 40 | * @Field(listOf=Todo::class) 41 | * @return Todo[] 42 | */ 43 | public static function todos(): array 44 | { 45 | $parent = new Todo('Parent todo'); 46 | $todo = new Todo('Something to do'); 47 | $todo->parent = $parent; 48 | return [$todo]; 49 | } 50 | 51 | public static function dynamicFields(): array 52 | { 53 | return map(range(1, 10), fn(int $i) => (new Field())->name("index$i")->type('Int')->resolve(always($i))); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/graphql-annotations/src/Subscription.php: -------------------------------------------------------------------------------- 1 | title = $title; 36 | $this->status = TodoStatus::TODO; 37 | } 38 | 39 | /** 40 | * @Field(type = TodoStatus::class) 41 | * @param Todo $todo 42 | * @return int 43 | */ 44 | public static function status(Todo $todo): int 45 | { 46 | return $todo->status; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/graphql-annotations/src/TodoStatus.php: -------------------------------------------------------------------------------- 1 | start(); 17 | -------------------------------------------------------------------------------- /examples/graphql/.gitignore: -------------------------------------------------------------------------------- 1 | /db.sqlite 2 | /.env -------------------------------------------------------------------------------- /examples/graphql/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "schema.graphql", 3 | "extensions": { 4 | "endpoints": { 5 | "siler_graphql": { 6 | "url": "http://localhost:8000", 7 | "subscription": { 8 | "url": "ws://localhost:8001" 9 | } 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /examples/graphql/README.md: -------------------------------------------------------------------------------- 1 | # Siler GraphQL Example 2 | 3 | This showcases: 4 | - Queries 5 | - Mutations 6 | - Subscriptions (+filters) 7 | - Directives 8 | - File uploads 9 | 10 | If you miss something you would like to see how is done with Siler, feel free to request. 11 | 12 | - Use the `swoole.php` file to see it working on top of Swoole runtime (Docker compose provided: `docker-compose up`). 13 | 14 | - Use `sapi.php` to see how it could be done with regular PHP over (Fast)CGI (`php -S localhost:8000 sapi.php`) *(subscriptions not supported)*. 15 | -------------------------------------------------------------------------------- /examples/graphql/directives.php: -------------------------------------------------------------------------------- 1 | function (callable $resolver): Closure { 9 | return function ($root, $args, $context, $info) use ($resolver): string { 10 | $value = $resolver($root, $args, $context, $info); 11 | return mb_strtoupper($value, 'UTF-8'); 12 | }; 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /examples/graphql/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | api: 4 | container_name: siler_example_graphql 5 | image: phpswoole/swoole 6 | volumes: 7 | - ../../:/var/www 8 | ports: 9 | - 8000:8000 10 | - 8001:8001 11 | environment: 12 | AUTORELOAD_PROGRAMS: "swoole" 13 | AUTORELOAD_ANY_FILES: 0 14 | entrypoint: php /var/www/examples/graphql/swoole.php 15 | -------------------------------------------------------------------------------- /examples/graphql/filters.php: -------------------------------------------------------------------------------- 1 | function (array $payload, array $vars) { 7 | return $payload['room_name'] == $vars['roomName']; 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /examples/graphql/resolvers.php: -------------------------------------------------------------------------------- 1 | function ($room) { 18 | return R::findAll('message', 'room_id = ?', [$room['id']]); 19 | } 20 | ]; 21 | 22 | $queries = [ 23 | 'rooms' => function () { 24 | return R::findAll('room'); 25 | }, 26 | 'messages' => function ($_, $args) use ($room_by_name) { 27 | $room_name = array_get_str($args, 'roomName'); 28 | $room = $room_by_name($room_name); 29 | return R::find('message', 'room_id = ?', [$room['id']]); 30 | } 31 | ]; 32 | 33 | $mutations = [ 34 | 'start' => function ($_, $args) { 35 | $room_name = array_get_str($args, 'roomName'); 36 | 37 | $room = R::dispense('room'); 38 | $room['name'] = $room_name; 39 | 40 | R::store($room); 41 | 42 | return $room; 43 | }, 44 | 'chat' => function ($_, $args) use ($room_by_name) { 45 | $room_name = array_get_str($args, 'roomName'); 46 | $body = array_get_str($args, 'body'); 47 | 48 | $room = $room_by_name($room_name); 49 | 50 | $message = R::dispense('message'); 51 | $message['roomId'] = $room['id']; 52 | $message['body'] = $body; 53 | $message['timestamp'] = new DateTime(); 54 | 55 | R::store($message); 56 | 57 | $message['roomName'] = $room_name; 58 | // For the inbox filter 59 | GraphQL\publish('inbox', $message); 60 | // <- Exactly what "inbox" will receive 61 | return $message; 62 | }, 63 | 'close' => function ($_, array $args) use ($room_by_name): bool { 64 | $room_name = array_get_str($args, 'roomName'); 65 | $room = $room_by_name($room_name); 66 | 67 | if ($room === null) { 68 | return false; 69 | } 70 | 71 | R::trash($room); 72 | return true; 73 | }, 74 | 'upload' => function ($_, $args) { 75 | $file = array_get_arr($args, 'file'); 76 | move_uploaded_file($file['tmp_name'], __DIR__ . "/uploads/{$file['name']}"); 77 | return ['name' => $file['name']]; 78 | }, 79 | ]; 80 | 81 | $subscriptions = [ 82 | 'inbox' => function ($message) { 83 | return $message; 84 | } 85 | ]; 86 | 87 | return [ 88 | 'Room' => $room, 89 | 'Query' => $queries, 90 | 'Mutation' => $mutations, 91 | 'Subscription' => $subscriptions, 92 | ]; 93 | -------------------------------------------------------------------------------- /examples/graphql/sapi.php: -------------------------------------------------------------------------------- 1 | ['roles' => ['inbox']]]; 30 | }); 31 | 32 | listen(ON_OPERATION, function (array $subscription, $_, $context) { 33 | if ($subscription['name'] === 'inbox') { 34 | if (empty($context['user'])) { 35 | throw new UserError('Unauthenticated', 401); 36 | } 37 | 38 | if (!in_array('inbox', $context['user']['roles'])) { 39 | throw new UserError('Unauthorized', 403); 40 | } 41 | } 42 | }); 43 | 44 | $resolvers = require_once __DIR__ . '/resolvers.php'; 45 | $directives = require_once __DIR__ . '/directives.php'; 46 | $root_value = []; 47 | $context = []; 48 | 49 | $type_defs = file_get_contents(__DIR__ . '/schema.graphql'); 50 | $schema = schema($type_defs, $resolvers); 51 | directives($directives); 52 | 53 | Route\post('/graphql', fn() => init($schema, $root_value, $context)); 54 | Route\get('/graphiql', puts(graphiql())); 55 | -------------------------------------------------------------------------------- /examples/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | directive @upper on FIELD 2 | 3 | scalar Upload 4 | 5 | type Message { 6 | id: ID! 7 | roomId: Int! 8 | body: String! 9 | timestamp: String! 10 | } 11 | 12 | type Room { 13 | id: ID! 14 | name: String! 15 | messages: [Message!]! 16 | } 17 | 18 | type File { 19 | name: String! 20 | } 21 | 22 | type Query { 23 | messages(roomName: String!): [Message!]! 24 | rooms: [Room!]! 25 | } 26 | 27 | type Mutation { 28 | start(roomName: String!): Room! 29 | chat(roomName: String!, body: String!): Message! 30 | close(roomName: String!): Boolean! 31 | upload(file: Upload): File! 32 | } 33 | 34 | type Subscription { 35 | inbox(roomName: String!): Message! 36 | } 37 | -------------------------------------------------------------------------------- /examples/graphql/swoole.php: -------------------------------------------------------------------------------- 1 | ['roles' => ['inbox']]]; 32 | }); 33 | 34 | GraphQL\listen(GraphQL\ON_OPERATION, function (array $subscription, $_, $context) { 35 | if ($subscription['name'] === 'inbox') { 36 | if (empty($context['user'])) { 37 | throw new UserError('Unauthenticated', 401); 38 | } 39 | 40 | if (!in_array('inbox', $context['user']['roles'])) { 41 | throw new UserError('Unauthorized', 403); 42 | } 43 | } 44 | }); 45 | 46 | $resolvers = require_once __DIR__ . '/resolvers.php'; 47 | $directives = require_once __DIR__ . '/directives.php'; 48 | $filters = require_once __DIR__ . '/filters.php'; 49 | $root_value = []; 50 | $context = []; 51 | 52 | $type_defs = file_get_contents(__DIR__ . '/schema.graphql'); 53 | $schema = GraphQL\schema($type_defs, $resolvers); 54 | GraphQL\directives($directives); 55 | 56 | $manager = GraphQL\subscriptions_manager($schema, $filters, $root_value, $context); 57 | 58 | $server = Swoole\graphql_subscriptions($manager, 8001); 59 | $server->set(['upload_tmp_dir' => __DIR__ . '/uploads']); 60 | 61 | Swoole\http_server_port($server, function () use ($schema, $root_value, $context) { 62 | Route\post('/graphql', fn() => Response\json(GraphQL\execute($schema, GraphQL\request()->toArray(), $root_value, $context))); 63 | Route\get('/graphiql', fn() => Response\html(GraphQL\graphiql())); 64 | }, 8000); 65 | 66 | $server->start(); 67 | -------------------------------------------------------------------------------- /examples/graphql/uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /examples/grpc/.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | !Dockerfile 3 | -------------------------------------------------------------------------------- /examples/grpc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster-slim 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | zlib1g-dev git build-essential autoconf libtool pkg-config apt-transport-https wget gnupg2 golang-go 5 | 6 | RUN go get -u google.golang.org/grpc \ 7 | && go get -u github.com/golang/protobuf/protoc-gen-go 8 | 9 | RUN sh -c 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' \ 10 | && sh -c 'wget -qO- https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list' \ 11 | && apt-get update && apt-get install -y dart \ 12 | && /usr/lib/dart/bin/pub global activate protoc_plugin 13 | 14 | RUN git clone -b v1.25.0 https://github.com/grpc/grpc.git \ 15 | && cd grpc && git submodule update --init && make 16 | 17 | RUN touch ~/.bashrc \ 18 | && echo "export GOPATH=\$HOME/go" >> ~/.bashrc \ 19 | && echo "export PATH=/grpc/bins/opt:/grpc/bins/opt/protobuf:\$GOPATH/bin:\$HOME/.pub-cache/bin:\$PATH" >> ~/.bashrc 20 | -------------------------------------------------------------------------------- /examples/grpc/README.md: -------------------------------------------------------------------------------- 1 | Docker-powered `protoc` and gRPC plugins. 2 | 3 | ```powershell 4 | docker build -t proto_gen . 5 | ``` 6 | 7 | ```powershell 8 | docker run --rm -v "$(pwd):/gen" -w /gen proto_gen bash -l ./proto_gen.sh 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/grpc/clients/dart/.gitignore: -------------------------------------------------------------------------------- 1 | .packages 2 | .dart_tool/ 3 | -------------------------------------------------------------------------------- /examples/grpc/clients/dart/bin/client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:grpc/grpc.dart'; 4 | import 'package:helloworld/src/generated/helloworld.pb.dart'; 5 | import 'package:helloworld/src/generated/helloworld.pbgrpc.dart'; 6 | 7 | Future main(List args) async { 8 | final channel = ClientChannel('localhost', 9 | port: 9090, 10 | options: 11 | const ChannelOptions(credentials: ChannelCredentials.insecure())); 12 | final stub = GreeterClient(channel); 13 | 14 | try { 15 | final response = await stub.sayHello(HelloRequest()..name = 'Siler'); 16 | print('Greeter client received: ${response.message}'); 17 | } catch (e) { 18 | print('Caught error: $e'); 19 | } 20 | await channel.shutdown(); 21 | } 22 | -------------------------------------------------------------------------------- /examples/grpc/clients/dart/lib/src/generated/helloworld.pbenum.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: helloworld.proto 4 | // 5 | // @dart = 2.3 6 | // ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type 7 | 8 | -------------------------------------------------------------------------------- /examples/grpc/clients/dart/lib/src/generated/helloworld.pbgrpc.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: helloworld.proto 4 | // 5 | // @dart = 2.3 6 | // ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type 7 | 8 | import 'dart:async' as $async; 9 | import 'dart:core' as $core; 10 | 11 | import 'package:grpc/service_api.dart' as $grpc; 12 | 13 | import 'helloworld.pb.dart' as $0; 14 | 15 | export 'helloworld.pb.dart'; 16 | 17 | class GreeterClient extends $grpc.Client { 18 | static final _$sayHello = $grpc.ClientMethod<$0.HelloRequest, $0.HelloReply>( 19 | '/helloworld.Greeter/sayHello', 20 | ($0.HelloRequest value) => value.writeToBuffer(), 21 | ($core.List<$core.int> value) => $0.HelloReply.fromBuffer(value)); 22 | 23 | GreeterClient($grpc.ClientChannel channel, {$grpc.CallOptions options}) 24 | : super(channel, options: options); 25 | 26 | $grpc.ResponseFuture<$0.HelloReply> sayHello($0.HelloRequest request, 27 | {$grpc.CallOptions options}) { 28 | final call = $createCall(_$sayHello, $async.Stream.fromIterable([request]), 29 | options: options); 30 | return $grpc.ResponseFuture(call); 31 | } 32 | } 33 | 34 | abstract class GreeterServiceBase extends $grpc.Service { 35 | $core.String get $name => 'helloworld.Greeter'; 36 | 37 | GreeterServiceBase() { 38 | $addMethod($grpc.ServiceMethod<$0.HelloRequest, $0.HelloReply>( 39 | 'sayHello', 40 | sayHello_Pre, 41 | false, 42 | false, 43 | ($core.List<$core.int> value) => $0.HelloRequest.fromBuffer(value), 44 | ($0.HelloReply value) => value.writeToBuffer())); 45 | } 46 | 47 | $async.Future<$0.HelloReply> sayHello_Pre( 48 | $grpc.ServiceCall call, $async.Future<$0.HelloRequest> request) async { 49 | return sayHello(call, await request); 50 | } 51 | 52 | $async.Future<$0.HelloReply> sayHello( 53 | $grpc.ServiceCall call, $0.HelloRequest request); 54 | } 55 | -------------------------------------------------------------------------------- /examples/grpc/clients/dart/lib/src/generated/helloworld.pbjson.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: helloworld.proto 4 | // 5 | // @dart = 2.3 6 | // ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type 7 | 8 | const HelloRequest$json = const { 9 | '1': 'HelloRequest', 10 | '2': const [ 11 | const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'}, 12 | ], 13 | }; 14 | 15 | const HelloReply$json = const { 16 | '1': 'HelloReply', 17 | '2': const [ 18 | const {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, 19 | ], 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /examples/grpc/clients/dart/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: helloworld 2 | description: Dart gRPC sample client. 3 | 4 | environment: 5 | sdk: '>=2.2.0 <3.0.0' 6 | 7 | dependencies: 8 | async: 9 | grpc: 10 | protobuf: 11 | -------------------------------------------------------------------------------- /examples/grpc/clients/go/.gitignore: -------------------------------------------------------------------------------- 1 | main.exe 2 | -------------------------------------------------------------------------------- /examples/grpc/clients/go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "google.golang.org/grpc" 10 | pb "google.golang.org/grpc/examples/helloworld/helloworld" 11 | ) 12 | 13 | const ( 14 | address = "localhost:9090" 15 | defaultName = "Siler" 16 | ) 17 | 18 | func main() { 19 | // Set up a connection to the server. 20 | conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) 21 | if err != nil { 22 | log.Fatalf("did not connect: %v", err) 23 | } 24 | defer conn.Close() 25 | c := pb.NewGreeterClient(conn) 26 | 27 | // Contact the server and print out its response. 28 | name := defaultName 29 | if len(os.Args) > 1 { 30 | name = os.Args[1] 31 | } 32 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 33 | defer cancel() 34 | r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) 35 | if err != nil { 36 | log.Fatalf("could not greet: %v", err) 37 | } 38 | log.Printf("Greeting: %s", r.GetMessage()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/grpc/clients/node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /examples/grpc/clients/node/client.js: -------------------------------------------------------------------------------- 1 | const grpc = require('grpc'); 2 | 3 | const messages = require('./helloworld_pb'); 4 | const services = require('./helloworld_grpc_pb'); 5 | 6 | const client = new services.GreeterClient('localhost:9090', 7 | grpc.credentials.createInsecure()); 8 | 9 | const request = new messages.HelloRequest(); 10 | request.setName('Siler'); 11 | 12 | client.sayHello(request, function (err, response) { 13 | console.log('Greeting:', response.getMessage()); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/grpc/clients/node/helloworld_grpc_pb.js: -------------------------------------------------------------------------------- 1 | // GENERATED CODE -- DO NOT EDIT! 2 | 3 | 'use strict'; 4 | var grpc = require('grpc'); 5 | var helloworld_pb = require('./helloworld_pb.js'); 6 | 7 | function serialize_helloworld_HelloReply(arg) { 8 | if (!(arg instanceof helloworld_pb.HelloReply)) { 9 | throw new Error('Expected argument of type helloworld.HelloReply'); 10 | } 11 | return new Buffer(arg.serializeBinary()); 12 | } 13 | 14 | function deserialize_helloworld_HelloReply(buffer_arg) { 15 | return helloworld_pb.HelloReply.deserializeBinary(new Uint8Array(buffer_arg)); 16 | } 17 | 18 | function serialize_helloworld_HelloRequest(arg) { 19 | if (!(arg instanceof helloworld_pb.HelloRequest)) { 20 | throw new Error('Expected argument of type helloworld.HelloRequest'); 21 | } 22 | return new Buffer(arg.serializeBinary()); 23 | } 24 | 25 | function deserialize_helloworld_HelloRequest(buffer_arg) { 26 | return helloworld_pb.HelloRequest.deserializeBinary(new Uint8Array(buffer_arg)); 27 | } 28 | 29 | 30 | var GreeterService = exports.GreeterService = { 31 | sayHello: { 32 | path: '/helloworld.Greeter/sayHello', 33 | requestStream: false, 34 | responseStream: false, 35 | requestType: helloworld_pb.HelloRequest, 36 | responseType: helloworld_pb.HelloReply, 37 | requestSerialize: serialize_helloworld_HelloRequest, 38 | requestDeserialize: deserialize_helloworld_HelloRequest, 39 | responseSerialize: serialize_helloworld_HelloReply, 40 | responseDeserialize: deserialize_helloworld_HelloReply, 41 | }, 42 | }; 43 | 44 | exports.GreeterClient = grpc.makeGenericClientConstructor(GreeterService); 45 | -------------------------------------------------------------------------------- /examples/grpc/clients/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@siler/grpc-node-client", 3 | "version": "1.0.0", 4 | "description": "Siler gRPC Node.js client example.", 5 | "main": "client.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/leocavalcante/siler.git" 12 | }, 13 | "keywords": [ 14 | "siler", 15 | "grpc", 16 | "node", 17 | "protobuf" 18 | ], 19 | "author": "leocavalcante ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/leocavalcante/siler/issues" 23 | }, 24 | "homepage": "https://github.com/leocavalcante/siler#readme", 25 | "dependencies": { 26 | "google-protobuf": "^3.11.2", 27 | "grpc": "^1.24.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/grpc/clients/php/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /examples/grpc/clients/php/bin/client.php: -------------------------------------------------------------------------------- 1 | ChannelCredentials::createInsecure(), 17 | ]); 18 | 19 | $request = new HelloRequest(); 20 | $request->setName('Siler'); 21 | 22 | /** @var HelloReply|null $reply */ 23 | list($reply, $status) = $greeter->sayHello($request)->wait(); 24 | 25 | if ($reply === null) { 26 | echo $status->details, PHP_EOL; 27 | return; 28 | } 29 | 30 | echo $reply->getMessage(), PHP_EOL; 31 | }); 32 | -------------------------------------------------------------------------------- /examples/grpc/clients/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "siler/grpc", 3 | "description": "Siler gRPC PHP client example", 4 | "type": "project", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "leocavalcante", 9 | "email": "lc@leocavalcante.com" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "require": {}, 14 | "autoload": { 15 | "psr-4": { 16 | "": "src/" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/grpc/clients/php/src/GPBMetadata/Helloworld.php: -------------------------------------------------------------------------------- 1 | internalAddGeneratedFile(hex2bin( 21 | "0aae010a1068656c6c6f776f726c642e70726f746f120a68656c6c6f776f" . 22 | "726c64221c0a0c48656c6c6f52657175657374120c0a046e616d65180120" . 23 | "012809221d0a0a48656c6c6f5265706c79120f0a076d6573736167651801" . 24 | "2001280932490a0747726565746572123e0a0873617948656c6c6f12182e" . 25 | "68656c6c6f776f726c642e48656c6c6f526571756573741a162e68656c6c" . 26 | "6f776f726c642e48656c6c6f5265706c792200620670726f746f33" 27 | ), true); 28 | 29 | static::$is_initialized = true; 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /examples/grpc/clients/php/src/Helloworld/GreeterClient.php: -------------------------------------------------------------------------------- 1 | _simpleRequest('/helloworld.Greeter/sayHello', 33 | $argument, 34 | ['\Helloworld\HelloReply', 'decode'], 35 | $metadata, $options); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /examples/grpc/clients/php/src/Helloworld/HelloReply.php: -------------------------------------------------------------------------------- 1 | helloworld.HelloReply 13 | */ 14 | class HelloReply extends Message 15 | { 16 | /** 17 | * Generated from protobuf field string message = 1; 18 | */ 19 | private $message = ''; 20 | 21 | /** 22 | * Constructor. 23 | * 24 | * @param array $data { 25 | * Optional. Data for populating the Message object. 26 | * 27 | * @type string $message 28 | * } 29 | */ 30 | public function __construct($data = NULL) 31 | { 32 | Helloworld::initOnce(); 33 | parent::__construct($data); 34 | } 35 | 36 | /** 37 | * Generated from protobuf field string message = 1; 38 | * @return string 39 | */ 40 | public function getMessage() 41 | { 42 | return $this->message; 43 | } 44 | 45 | /** 46 | * Generated from protobuf field string message = 1; 47 | * @param string $var 48 | * @return $this 49 | */ 50 | public function setMessage($var) 51 | { 52 | GPBUtil::checkString($var, True); 53 | $this->message = $var; 54 | 55 | return $this; 56 | } 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /examples/grpc/clients/php/src/Helloworld/HelloRequest.php: -------------------------------------------------------------------------------- 1 | helloworld.HelloRequest 13 | */ 14 | class HelloRequest extends Message 15 | { 16 | /** 17 | * Generated from protobuf field string name = 1; 18 | */ 19 | private $name = ''; 20 | 21 | /** 22 | * Constructor. 23 | * 24 | * @param array $data { 25 | * Optional. Data for populating the Message object. 26 | * 27 | * @type string $name 28 | * } 29 | */ 30 | public function __construct($data = NULL) 31 | { 32 | Helloworld::initOnce(); 33 | parent::__construct($data); 34 | } 35 | 36 | /** 37 | * Generated from protobuf field string name = 1; 38 | * @return string 39 | */ 40 | public function getName() 41 | { 42 | return $this->name; 43 | } 44 | 45 | /** 46 | * Generated from protobuf field string name = 1; 47 | * @param string $var 48 | * @return $this 49 | */ 50 | public function setName($var) 51 | { 52 | GPBUtil::checkString($var, True); 53 | $this->name = $var; 54 | 55 | return $this; 56 | } 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /examples/grpc/proto_gen.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | protoc \ 3 | --php_out=clients/php/src --grpc_out=clients/php/src \ 4 | --plugin=protoc-gen-grpc=/grpc/bins/opt/grpc_php_plugin \ 5 | -Iprotos helloworld.proto 6 | 7 | protoc \ 8 | --go_out=plugins=grpc:clients/go \ 9 | -Iprotos helloworld.proto 10 | 11 | protoc \ 12 | --dart_out=grpc:clients/dart/lib/src/generated \ 13 | -Iprotos helloworld.proto 14 | 15 | protoc \ 16 | --js_out=import_style=commonjs,binary:clients/node --grpc_out=clients/node \ 17 | --plugin=protoc-gen-grpc=/grpc/bins/opt/grpc_node_plugin \ 18 | -Iprotos helloworld.proto 19 | -------------------------------------------------------------------------------- /examples/grpc/protos/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package helloworld; 4 | 5 | service Greeter { 6 | rpc sayHello (HelloRequest) returns (HelloReply) { 7 | } 8 | } 9 | 10 | message HelloRequest { 11 | string name = 1; 12 | } 13 | 14 | message HelloReply { 15 | string message = 1; 16 | } 17 | -------------------------------------------------------------------------------- /examples/grpc/server.php: -------------------------------------------------------------------------------- 1 | setMessage('Hello ' . $request->getName()); 19 | return $reply; 20 | } 21 | } 22 | 23 | $services = ['helloworld.Greeter' => new Greeter()]; 24 | $server = Grpc\server($services); 25 | 26 | echo "Listening at localhost:9090\n"; 27 | $server->start(); 28 | -------------------------------------------------------------------------------- /examples/hello-world/README.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 4 ways to Hello World in [Siler](https://github.com/leocavalcante/siler). My *f*avorite is: 3 | 4 | ```php 5 | use Siler\Functional as λ; 6 | use Siler\Route; 7 | 8 | Route\get('/functional-hello', λ\puts('Hello Functional World')); 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/hello-world/hello-world.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Siler's Hello World 6 | 7 | 8 |

Hello World!

9 |

Name

10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/hello-world/index.php: -------------------------------------------------------------------------------- 1 | handle($req->withAttribute('name', $name)); 19 | }; 20 | 21 | // A PSR-15 Middleware 22 | $attach = function ($req, $handler) { 23 | return json(['hello' => $req->getAttribute('name')]); 24 | }; 25 | 26 | // Stratigility default pipeline 27 | pipe(middleware($greet)); 28 | pipe(middleware($attach)); 29 | 30 | // A PSR-7 Server Request from Globals 31 | $req = request(); 32 | 33 | // A PSR-7 Response Message after pipline marshaling 34 | $res = handle($req); 35 | 36 | // Standard API emitter (header, echo, http_status_code) 37 | sapi_emit($res); 38 | 39 | /* 40 | * > curl http://localhost:8080 41 | * < {"hello":"world"}. 42 | */ 43 | 44 | /* 45 | * > curl http://localhost:8080?name=leo 46 | * < {"hello":"leo"}. 47 | */ 48 | -------------------------------------------------------------------------------- /examples/laminas/routes/index.php: -------------------------------------------------------------------------------- 1 | withAttribute('user', $user); 21 | 22 | return $handler->handle($request); 23 | }; 24 | 25 | $homeHandler = function () { 26 | return Diactoros\json('welcome'); 27 | }; 28 | 29 | $adminHandler = function ($request) { 30 | return Diactoros\json(['user' => $request->getAttribute('user')]); 31 | }; 32 | 33 | $secretHandler = function ($request) { 34 | return Diactoros\json(['user' => $request->getAttribute('user')]); 35 | }; 36 | 37 | Stratigility\pipe($userMiddleware, 'auth'); 38 | 39 | $request = Diactoros\request(); 40 | $response = Route\matching([ 41 | Route\get('/', $homeHandler, $request), 42 | Route\get('/admin', Stratigility\process($request, 'auth')($adminHandler), $request), 43 | Route\get('/secret', Stratigility\process($request, 'auth')($secretHandler), $request), 44 | Diactoros\json('not found', 404) 45 | ]); 46 | 47 | HttpHandlerRunner\sapi_emit($response); 48 | -------------------------------------------------------------------------------- /examples/mail/.env.example: -------------------------------------------------------------------------------- 1 | SMTP_HOST=smtp.sendgrid.net 2 | SMTP_PORT=587 3 | SMTP_USERNAME=apikey 4 | SMTP_PASSWORD= 5 | 6 | FROM=from@email.com 7 | TO=to@email.com -------------------------------------------------------------------------------- /examples/mail/.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /examples/mail/swiftmailer.php: -------------------------------------------------------------------------------- 1 | 'debug']); 11 | Log\info('info', ['level' => 'info']); 12 | Log\notice('notice', ['level' => 'notice']); 13 | Log\warning('warning', ['level' => 'warning']); 14 | Log\error('error', ['level' => 'error']); 15 | Log\critical('critical', ['level' => 'critical']); 16 | Log\alert('alert', ['level' => 'alert']); 17 | Log\emergency('emergency', ['level' => 'emergency']); 18 | -------------------------------------------------------------------------------- /examples/monolog/siler.log: -------------------------------------------------------------------------------- 1 | [2019-01-27 15:27:26] monolog_default_channel.DEBUG: Debug {"level":"debug"} [] 2 | [2019-01-27 15:27:58] log.DEBUG: Debug {"level":"debug"} [] 3 | [2019-01-27 15:29:36] log.DEBUG: debug {"level":"debug"} [] 4 | [2019-01-27 15:29:36] log.INFO: info {"level":"info"} [] 5 | [2019-01-27 15:29:36] log.NOTICE: notice {"level":"notice"} [] 6 | [2019-01-27 15:29:36] log.WARNING: warning {"level":"warning"} [] 7 | [2019-01-27 15:29:36] log.ERROR: error {"level":"error"} [] 8 | [2019-01-27 15:29:36] log.CRITICAL: critical {"level":"critical"} [] 9 | [2019-01-27 15:29:36] log.ALERT: alert {"level":"alert"} [] 10 | [2019-01-27 15:29:36] log.EMERGENCY: emergency {"level":"emergency"} [] 11 | -------------------------------------------------------------------------------- /examples/psr7-diactoros/index.php: -------------------------------------------------------------------------------- 1 | getQueryParams())); 21 | }); 22 | 23 | twig('examples/psr7-diactoros'); 24 | 25 | // call like: /to-html?foo=bar 26 | get('/to-html', function () { 27 | // outputs a table with foo bar, see: template.twig 28 | sapi_emit(html(render('template.twig', ['query' => request()->getQueryParams()]))); 29 | }); 30 | 31 | // call like: /to-text?foo=bar 32 | get('/to-text', function () { 33 | // output: foo=bar 34 | sapi_emit(text(http_build_query(request()->getQueryParams()))); 35 | }); 36 | -------------------------------------------------------------------------------- /examples/psr7-diactoros/template.twig: -------------------------------------------------------------------------------- 1 | 2 | {% for key, val in query %} 3 | 4 | 5 | 6 | 7 | {% endfor %} 8 |
{{ key }}{{ val }}
9 | -------------------------------------------------------------------------------- /examples/psr7-route/index.php: -------------------------------------------------------------------------------- 1 | getQueryParams(), 'salute', 'Olá'); 20 | 21 | return Diactoros\text("{$salute} {$params['name']}"); 22 | }, 23 | $request 24 | ), 25 | 26 | Route\get( 27 | '/', 28 | function () { 29 | return Diactoros\text('hello world'); 30 | }, 31 | $request 32 | ), 33 | 34 | Diactoros\text('not found', 404) 35 | ]); 36 | 37 | HttpHandlerRunner\sapi_emit($response); 38 | -------------------------------------------------------------------------------- /examples/route-any/index.php: -------------------------------------------------------------------------------- 1 | 6 | But can you still call it MVC, right? 7 | -------------------------------------------------------------------------------- /examples/route-files/controllers/about.get.php: -------------------------------------------------------------------------------- 1 | name = $name; 12 | $contact->message = $message; 13 | $contact->date = new DateTime(); 14 | 15 | R::store($contact); 16 | 17 | redirect('/about'); 18 | -------------------------------------------------------------------------------- /examples/route-files/controllers/index.get.php: -------------------------------------------------------------------------------- 1 | About 5 | 6 |

Contacts

7 | 8 |
9 | {% for contact in contacts %} 10 |
{{ contact.name }}
11 |
12 |

{{ contact.message }}

13 |

{{ contact.date|date }}

14 |
15 | {% endfor %} 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /examples/route-files/views/contact.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block content %} 4 |

Contact

5 | 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /examples/route-files/views/home.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block content %} 4 |

Home

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /examples/route-files/views/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Route Files 6 | 7 | 8 |
9 | 14 |
15 | 16 |
17 | {% block content %}{% endblock %} 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/route-not-found/books/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swoole Chat 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 | 15 | 36 | 37 | -------------------------------------------------------------------------------- /examples/swoole-chat/server.php: -------------------------------------------------------------------------------- 1 | data); 9 | }; 10 | 11 | Swoole\websocket_hooks([ 12 | 'open' => puts("New connection\n"), 13 | 'close' => puts("Someone leaves\n") 14 | ]); 15 | 16 | Swoole\websocket($echo)->start(); 17 | -------------------------------------------------------------------------------- /examples/swoole/README.md: -------------------------------------------------------------------------------- 1 | # Siler ❤️ Swoole 2 | 3 | * [Siler is](https://github.com/leocavalcante/siler) 4 | * [Swoole is](https://www.swoole.co.uk/) 5 | 6 | Flat files and plain-old PHP functions rocking on a production-grade, high-performance, scalable, concurrent and non-blocking HTTP server. 7 | 8 | It uses [Dwoole](https://github.com/leocavalcante/dwoole) for a cross-platform and auto-restartable development environment. -------------------------------------------------------------------------------- /examples/swoole/api/todos.php: -------------------------------------------------------------------------------- 1 | 1, 13 | 'text' => 'foo' 14 | ], 15 | [ 16 | 'id' => 2, 17 | 'text' => 'bar' 18 | ], 19 | [ 20 | 'id' => 3, 21 | 'text' => 'baz' 22 | ] 23 | ]; 24 | 25 | return function () use ($todos) { 26 | Swoole\json($todos); 27 | }; 28 | -------------------------------------------------------------------------------- /examples/swoole/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | swoole: 4 | container_name: siler_swoole 5 | image: leocavalcante/dwoole:dev 6 | ports: 7 | - '9501:9501' 8 | volumes: 9 | - ../../:/app 10 | environment: 11 | ENTRY_POINT_FILE: /app/examples/swoole/index.php 12 | -------------------------------------------------------------------------------- /examples/swoole/index.php: -------------------------------------------------------------------------------- 1 | set([ 21 | 'enable_static_handler' => true, 22 | 'document_root' => __DIR__ . '/public', 23 | ]); 24 | $server->start(); 25 | -------------------------------------------------------------------------------- /examples/swoole/pages/_layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Siler + Swoole 8 | 9 | 10 | 11 | 12 | {% block page %}{% endblock %} 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/swoole/pages/home.php: -------------------------------------------------------------------------------- 1 | get['name'] ?? 'World'; 10 | Swoole\emit(Twig\render('home.twig', ['name' => $name])); 11 | }; 12 | -------------------------------------------------------------------------------- /examples/swoole/pages/home.twig: -------------------------------------------------------------------------------- 1 | {% extends "_layout.twig" %} 2 | 3 | {% block page %} 4 |

Hello {{ name }}

5 | {% endblock %} -------------------------------------------------------------------------------- /examples/swoole/public/assets/scripts.js: -------------------------------------------------------------------------------- 1 | console.log('Siler ❤️ Swoole') 2 | -------------------------------------------------------------------------------- /examples/swoole/public/assets/styles.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /examples/twig/home.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Siler\Twig 6 | 7 | 8 |

Hello {{ name }}

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/twig/index.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | Siler's Code Style 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | tests/Swoole/Swoole.php 13 | 14 | 15 | 16 | src 17 | tests 18 | src/facades.php 19 | 20 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | src 12 | 13 | 14 | src/Grpc 15 | src/Swoole 16 | src/facades.php 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | tests/Unit 27 | 28 | 29 | tests/Integration 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /siler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leocavalcante/siler/a948888c52dfa5eeee9fda8ce13ded1c422fcbaa/siler.png -------------------------------------------------------------------------------- /src/Diactoros/Diactoros.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | function init(string $path): array 19 | { 20 | $dot_env = Dotenv::createImmutable($path); 21 | return $dot_env->load(); 22 | } 23 | 24 | /** 25 | * Get an environment value or fallback to the given default. 26 | * 27 | * @param string|null $key 28 | * @param string|null $default A default when the key do not exists 29 | * @return string|null|array 30 | */ 31 | function env(?string $key = null, ?string $default = null) 32 | { 33 | /** @var array $_SERVER */ 34 | return array_get($_SERVER, $key, $default); 35 | } 36 | 37 | /** 38 | * Returns an environment variable as an integer. 39 | * 40 | * @param string $key 41 | * @param int|null $default 42 | * @return int|null 43 | */ 44 | function int_val(string $key, ?int $default = null): ?int 45 | { 46 | $val = env($key); 47 | 48 | if ($val === null) { 49 | return $default; 50 | } 51 | 52 | if (is_numeric($val)) { 53 | return (int) $val; 54 | } 55 | 56 | return $default; 57 | } 58 | 59 | /** 60 | * Returns an environment variable as an boolean. 61 | * 62 | * @param string $key 63 | * @param bool|null $default 64 | * @return bool|null 65 | */ 66 | function bool_val(string $key, ?bool $default = null): ?bool 67 | { 68 | $val = env($key); 69 | 70 | if ($val === null) { 71 | return $default; 72 | } 73 | 74 | if ($val === 'false') { 75 | return false; 76 | } 77 | 78 | if ($val === '[]') { 79 | return false; 80 | } 81 | 82 | if ($val === '{}') { 83 | return false; 84 | } 85 | 86 | return (bool) $val; 87 | } 88 | 89 | /** 90 | * Checks for the presence of an environment variable. 91 | * 92 | * @param string $key 93 | * @return true 94 | */ 95 | function requires(string $key): bool 96 | { 97 | if (\array_key_exists($key, $_ENV)) { 98 | return true; 99 | } 100 | 101 | throw new UnexpectedValueException("$key is not set in the environment variables"); 102 | } 103 | -------------------------------------------------------------------------------- /src/Encoder/Json.php: -------------------------------------------------------------------------------- 1 | 18 | * @return SplFileInfo[] 19 | */ 20 | function recursively_iterated_directory(string $dirname, string $regex = '/.*/', int $mode = RegexIterator::MATCH): array 21 | { 22 | $dir_iterator = new RecursiveDirectoryIterator($dirname); 23 | $iterator = new RecursiveIteratorIterator($dir_iterator); 24 | $regexp_iterator = new RegexIterator($iterator, $regex, $mode); 25 | 26 | $list = []; 27 | 28 | /** @var SplFileInfo $spl_file_info */ 29 | foreach ($regexp_iterator as $spl_file_info) { 30 | $list[] = $spl_file_info; 31 | } 32 | 33 | return $list; 34 | } 35 | 36 | /** 37 | * Alias for recursively_iterated_directory(). 38 | * 39 | * @param string $dirname 40 | * @param string $regex 41 | * @param RegexIterator::MATCH|RegexIterator::GET_MATCH|RegexIterator::ALL_MATCHES|RegexIterator::SPLIT|RegexIterator::REPLACE $mode 42 | * @psalm-return list 43 | * @return SplFileInfo[] 44 | */ 45 | function recur_iter_dir(string $dirname, string $regex = '/.*/', int $mode = RegexIterator::MATCH): array 46 | { 47 | return recursively_iterated_directory($dirname, $regex, $mode); 48 | } 49 | 50 | /** 51 | * Loads and concatenates file contents. 52 | * 53 | * @param string[]|SplFileInfo[] $files 54 | * @param string $separator 55 | * 56 | * @return string 57 | */ 58 | function concat_files(array $files, string $separator = "\n"): string 59 | { 60 | $files = array_filter( 61 | $files, 62 | /** 63 | * @param string|SplFileInfo $file 64 | * @return bool 65 | */ 66 | static function ($file): bool { 67 | if ($file instanceof SplFileInfo) { 68 | $file->isFile(); 69 | } 70 | 71 | return is_file((string)$file); 72 | } 73 | ); 74 | 75 | $files = array_map( 76 | /** 77 | * @param string|SplFileInfo $file 78 | */ 79 | static function ($file): string { 80 | if ($file instanceof SplFileInfo) { 81 | return file_get_contents($file->getPathname()); 82 | } 83 | 84 | return file_get_contents($file); 85 | }, 86 | $files 87 | ); 88 | 89 | $contents = array_reduce($files, concat($separator), ''); 90 | 91 | return trim($contents); 92 | } 93 | 94 | /** 95 | * @param array ...$segments 96 | * 97 | * @return string 98 | */ 99 | function join_dir(...$segments): string 100 | { 101 | return implode(DIRECTORY_SEPARATOR, $segments); 102 | } 103 | -------------------------------------------------------------------------------- /src/Functional/Monad/Identity.php: -------------------------------------------------------------------------------- 1 | value = $value; 23 | } 24 | 25 | /** 26 | * @param callable|null $function 27 | * @return self|mixed 28 | * @psalm-param callable(T):(T|null)|null $function 29 | * @psalm-return self|T 30 | */ 31 | public function __invoke(callable $function = null) 32 | { 33 | if ($function === null) { 34 | return $this->value; 35 | } 36 | 37 | return new self($function($this->value)); 38 | } 39 | 40 | /** 41 | * @return mixed 42 | * @psalm-return T 43 | */ 44 | public function return() 45 | { 46 | return $this->value; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Functional/Monad/Maybe.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | class Maybe extends Identity 10 | { 11 | /** 12 | * @param callable|null $function 13 | * @return self|mixed 14 | * @psalm-param callable(T):(T|null)|null $function 15 | * @psalm-return self|T 16 | */ 17 | public function __invoke(callable $function = null) 18 | { 19 | if ($function === null) { 20 | return $this->return(); 21 | } 22 | 23 | return $this->bind($function); 24 | } 25 | 26 | /** 27 | * @param callable(T):(T|null) $function 28 | * @return self 29 | */ 30 | public function bind(callable $function): self 31 | { 32 | if ($this->value === null) { 33 | return new self(null); 34 | } 35 | 36 | return new self($function($this->value)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Functional/Monad/Monad.php: -------------------------------------------------------------------------------- 1 | fields = $values['value']; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/GraphQL/Annotation/Directive.php: -------------------------------------------------------------------------------- 1 | type = $type; 52 | return $this; 53 | } 54 | 55 | public function name(string $name): self 56 | { 57 | $this->name = $name; 58 | return $this; 59 | } 60 | 61 | public function description(string $description): self 62 | { 63 | $this->description = $description; 64 | return $this; 65 | } 66 | 67 | public function listOf(string $listOf): self 68 | { 69 | $this->listOf = $listOf; 70 | return $this; 71 | } 72 | 73 | public function nullable(bool $nullable): self 74 | { 75 | $this->nullable = $nullable; 76 | return $this; 77 | } 78 | 79 | public function nullableList(bool $nullableList): self 80 | { 81 | $this->nullableList = $nullableList; 82 | return $this; 83 | } 84 | 85 | public function resolve(?callable $resolve): self 86 | { 87 | $this->resolve = $resolve; 88 | return $this; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/GraphQL/Annotation/InputType.php: -------------------------------------------------------------------------------- 1 | format((string) static::FORMAT); 28 | } 29 | 30 | throw new Error('Don\'t know how to serialize non-DateTime'); 31 | } 32 | 33 | /** 34 | * @param Node $valueNode 35 | * @param array|null $variables 36 | * @return mixed 37 | * @throws Error 38 | */ 39 | public function parseLiteral(Node $valueNode, ?array $variables = null) 40 | { 41 | if ($valueNode instanceof StringValueNode) { 42 | return $this->parseValue($valueNode->value); 43 | } 44 | 45 | throw new Error(sprintf('Unable to parse non string literal as %s', $this->name)); 46 | } 47 | 48 | /** 49 | * @param mixed $value 50 | * @return mixed 51 | * @throws Error 52 | */ 53 | public function parseValue($value) 54 | { 55 | $date_time = false; 56 | 57 | if (\is_string($value)) { 58 | $date_time = DateTime::createFromFormat((string) static::FORMAT, $value); 59 | } 60 | 61 | if ($date_time === false) { 62 | throw new Error(sprintf("Error parsing $value as %s. Is it in %s format?", $this->name, (string)static::FORMAT)); 63 | } 64 | 65 | return $date_time; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/GraphQL/DateTimeScalar.php: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 36 | 37 | 44 | 48 | 52 | 53 | 58 | 59 | 60 | 61 | 62 |
Loading...
63 | 67 | 95 | 96 | 97 | HTML; 98 | } 99 | -------------------------------------------------------------------------------- /src/GraphQL/Request.php: -------------------------------------------------------------------------------- 1 | query = $query; 25 | $this->variables = $variables; 26 | $this->operationName = $operationName; 27 | } 28 | 29 | /** 30 | * @return array 31 | * @internal For legacy purposes, may be removed. 32 | */ 33 | public function toArray(): array 34 | { 35 | return [ 36 | 'query' => $this->query, 37 | 'variables' => $this->variables, 38 | 'operationName' => $this->operationName, 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/GraphQL/SubscriptionsConnection.php: -------------------------------------------------------------------------------- 1 | serializeToString()); 20 | } 21 | 22 | /** 23 | * @param string $data 24 | * @return string 25 | */ 26 | public static function pack(string $data): string 27 | { 28 | return pack('CN', 0, \strlen($data)) . $data; 29 | } 30 | 31 | /** 32 | * @param \ReflectionClass $message_class 33 | * @param string $value 34 | * @return Message|null 35 | * @throws \Exception 36 | */ 37 | public static function deserialize(\ReflectionClass $message_class, string $value): ?Message 38 | { 39 | if (empty($value)) { 40 | return null; 41 | } 42 | 43 | $value = self::unpack($value); 44 | 45 | /** @var Message $object */ 46 | $object = $message_class->newInstance(); 47 | $object->mergeFromString($value); 48 | 49 | return $object; 50 | } 51 | 52 | /** 53 | * @param string $data 54 | * @return string 55 | */ 56 | public static function unpack(string $data): string 57 | { 58 | // it's the way to verify the package length 59 | // 1 + 4 + data 60 | // $len = unpack('N', substr($data, 1, 4))[1]; 61 | // assert(strlen($data) - 5 === $len); 62 | return substr($data, 5); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/HttpHandlerRunner/HttpHandlerRunner.php: -------------------------------------------------------------------------------- 1 | emit($response); 22 | } 23 | -------------------------------------------------------------------------------- /src/Mail/SwiftMailer.php: -------------------------------------------------------------------------------- 1 | send($message); 37 | } 38 | 39 | /** 40 | * Sugar to create a new SwiftMailer Message. 41 | * 42 | * @param string $subject 43 | * @param array $from 44 | * @param array $to 45 | * @param string $body 46 | * @param string $contentType 47 | * 48 | * @return Swift_Message 49 | */ 50 | function message( 51 | string $subject, 52 | array $from, 53 | array $to, 54 | string $body, 55 | string $contentType = 'text/plain' 56 | ): Swift_Message { 57 | return (new Swift_Message()) 58 | ->setSubject($subject) 59 | ->setFrom($from) 60 | ->setTo($to) 61 | ->setBody($body, $contentType); 62 | } 63 | 64 | /** 65 | * Sugar to create a new SwiftMailer SMTP transport. 66 | * 67 | * @param string $host 68 | * @param int $port 69 | * @param string|null $username 70 | * @param string|null $password 71 | * 72 | * @return Swift_SmtpTransport 73 | */ 74 | function smtp(string $host, int $port, ?string $username = null, ?string $password = null): Swift_SmtpTransport 75 | { 76 | $transport = new Swift_SmtpTransport($host, $port); 77 | 78 | if ($username !== null) { 79 | $transport->setUsername($username); 80 | } 81 | 82 | if ($password !== null) { 83 | $transport->setPassword($password); 84 | } 85 | 86 | return $transport; 87 | } 88 | 89 | /** 90 | * Setup a Swift Mailer in the Siler Container. 91 | * 92 | * @param Swift_Transport $transport 93 | * 94 | * @return Swift_Mailer 95 | */ 96 | function mailer(Swift_Transport $transport): Swift_Mailer 97 | { 98 | $mailer = new Swift_Mailer($transport); 99 | Container\set(SWIFT_MAILER, $mailer); 100 | 101 | return $mailer; 102 | } 103 | -------------------------------------------------------------------------------- /src/Monolog/Loggers.php: -------------------------------------------------------------------------------- 1 | */ 18 | public static $predicates = []; 19 | 20 | /** 21 | * Returns the Logger identified by the channel. 22 | * 23 | * @param string $channel The log channel. 24 | * 25 | * @return Logger 26 | */ 27 | public static function getLogger(string $channel): Logger 28 | { 29 | if (empty(static::$loggers[$channel])) { 30 | static::$loggers[$channel] = new Logger($channel); 31 | } 32 | 33 | return static::$loggers[$channel]; 34 | } 35 | 36 | /** 37 | * @param int $level 38 | * @param bool|callable():bool $predicate 39 | */ 40 | public static function logIf(int $level, $predicate): void 41 | { 42 | static::$predicates[$level] = $predicate; 43 | } 44 | 45 | public static function gate(int $level): bool 46 | { 47 | if (!array_key_exists($level, self::$predicates)) { 48 | return true; 49 | } 50 | 51 | $pred = self::$predicates[$level]; 52 | 53 | if (is_callable($pred)) { 54 | return $pred(); 55 | } 56 | 57 | return $pred; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Prelude/Arr.php: -------------------------------------------------------------------------------- 1 | 1) { 20 | $key = array_shift($keys); 21 | 22 | // If the key doesn't exist at this depth, we will just create an empty array 23 | // to hold the next value, allowing us to create the arrays to hold final 24 | // values at the correct depth. Then we'll keep digging into the array. 25 | if (!isset($array[$key]) || !is_array($array[$key])) { 26 | $array[$key] = []; 27 | } 28 | 29 | $array = &$array[$key]; 30 | } 31 | 32 | /** @psalm-suppress MixedAssignment */ 33 | $array[array_shift($keys)] = $value; 34 | return $array; 35 | } 36 | 37 | /** 38 | * Creates an array of associative arrays using the first element as keys. 39 | * 40 | * @param array $arr 41 | * @return array 42 | */ 43 | function assoc(array $arr): array 44 | { 45 | /** @var array $head */ 46 | $head = $arr[0]; 47 | 48 | return array_map(static function (array $row) use ($head): array { 49 | return array_combine($head, $row); 50 | }, array_slice($arr, 1)); 51 | } 52 | -------------------------------------------------------------------------------- /src/Prelude/Dispatcher.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | private $listeners = []; 18 | 19 | /** 20 | * Listen for an event using its class name as pattern. 21 | * 22 | * @template E 23 | * @param string $eventClass 24 | * @psalm-param class-string $eventClass 25 | * @param callable(E):void $callback 26 | * @return $this 27 | */ 28 | public function listen(string $eventClass, callable $callback): self 29 | { 30 | if (empty($this->listeners[$eventClass])) { 31 | $this->listeners[$eventClass] = []; 32 | } 33 | 34 | $this->listeners[$eventClass][] = $callback; 35 | return $this; 36 | } 37 | 38 | /** 39 | * Dispatch the given event. 40 | * 41 | * @param object $event 42 | * @return object 43 | */ 44 | public function dispatch(object $event): object 45 | { 46 | $event_class = get_class($event); 47 | 48 | foreach ($this->listeners[$event_class] as $callback) { 49 | $callback($event); 50 | } 51 | 52 | return $event; 53 | } 54 | 55 | /** 56 | * Returns the listeners for the given event. 57 | * 58 | * @param object $event 59 | * @return iterable 60 | */ 61 | public function getListenersForEvent(object $event): iterable 62 | { 63 | $event_class = get_class($event); 64 | return $this->listeners[$event_class]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Prelude/Enum.php: -------------------------------------------------------------------------------- 1 | */ 15 | private static $constsMemo = []; 16 | 17 | /** 18 | * @param mixed $value 19 | * @return static 20 | * @throws ReflectionException 21 | */ 22 | public static function of($value): self 23 | { 24 | /** @psalm-suppress UnsafeInstantiation */ 25 | return new static($value); 26 | } 27 | 28 | /** 29 | * @return array 30 | * @throws ReflectionException 31 | */ 32 | protected static function consts(): array 33 | { 34 | $class_name = static::class; 35 | 36 | if (!\array_key_exists($class_name, self::$constsMemo)) { 37 | $reflection = new ReflectionClass($class_name); 38 | self::$constsMemo[$class_name] = $reflection->getConstants(); 39 | } 40 | 41 | return self::$constsMemo[$class_name]; 42 | } 43 | 44 | /** 45 | * @param mixed $value 46 | * @return bool 47 | * @throws ReflectionException 48 | */ 49 | protected static function valid($value): bool 50 | { 51 | return \in_array($value, array_values(static::consts()), true); 52 | } 53 | 54 | /** @var mixed */ 55 | private $value; 56 | 57 | /** 58 | * @param mixed $value 59 | * @throws ReflectionException 60 | */ 61 | public function __construct($value) 62 | { 63 | if (!static::valid($value)) { 64 | $class_name = static::class; 65 | throw new UnexpectedValueException("Invalid value ($value) for enum ($class_name)"); 66 | } 67 | 68 | $this->value = $value; 69 | } 70 | 71 | /** 72 | * @return mixed 73 | */ 74 | public function valueOf() 75 | { 76 | return $this->value; 77 | } 78 | 79 | /** 80 | * @param Enum $other 81 | * @return bool 82 | */ 83 | public function sameAs(Enum $other): bool 84 | { 85 | return $this->value === $other->value; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function __toString(): string 92 | { 93 | return (string) $this->value; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Prelude/FromArray.php: -------------------------------------------------------------------------------- 1 | getProperties() as $prop) { 24 | $prop_name = $prop->getName(); 25 | $key = snake_case($prop_name); 26 | 27 | if (array_key_exists($prop_name, $arr)) { 28 | $obj->{$prop_name} = $arr[$prop_name]; 29 | } elseif (array_key_exists($key, $arr)) { 30 | $obj->{$prop_name} = $arr[$key]; 31 | } 32 | } 33 | 34 | return $obj; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Prelude/FromArrayInterface.php: -------------------------------------------------------------------------------- 1 | $source 14 | * @return mixed 15 | * @psalm-return T 16 | */ 17 | function patch($target, array $source) 18 | { 19 | /** @psalm-suppress MixedAssignment */ 20 | foreach ($source as $key => $value) { 21 | $prop = lcfirst(camel_case($key)); 22 | 23 | if (property_exists($target, $prop)) { 24 | $target->{$prop} = $value; 25 | } 26 | } 27 | 28 | return $target; 29 | } 30 | -------------------------------------------------------------------------------- /src/Prelude/Patch.php: -------------------------------------------------------------------------------- 1 | slugify($input, $opts); 29 | } 30 | 31 | /** 32 | * Breaks a string into lines. 33 | * 34 | * @param string $input 35 | * 36 | * @return array 37 | */ 38 | function lines(string $input): array 39 | { 40 | return array_map(function (string $row): string { 41 | return trim($row); 42 | }, preg_split('/\n/', $input)); 43 | } 44 | 45 | /** 46 | * Checks if a string starts with another string. 47 | * 48 | * @param string $haystack 49 | * @param string $needle 50 | * @return bool 51 | */ 52 | function starts_with(string $haystack, string $needle): bool 53 | { 54 | return strpos($haystack, $needle) === 0; 55 | } 56 | 57 | /** 58 | * Checks if a string ends by another string. 59 | * 60 | * @param string $haystack 61 | * @param string $needle 62 | * @return bool 63 | */ 64 | function ends_with(string $haystack, string $needle): bool 65 | { 66 | return substr_compare($haystack, $needle, -strlen($needle)) === 0; 67 | } 68 | 69 | /** 70 | * Checks if a string contains another string. 71 | * 72 | * @param string $haystack 73 | * @param string $needle 74 | * @return bool 75 | */ 76 | function contains(string $haystack, string $needle): bool 77 | { 78 | return strpos($haystack, $needle) !== false; 79 | } 80 | 81 | /** 82 | * Converts a CamelCase string to snake_case. 83 | * 84 | * @param string $input 85 | * @return string 86 | */ 87 | function snake_case(string $input): string 88 | { 89 | return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $input)); 90 | } 91 | 92 | /** 93 | * Converts a snake_case string to CamelCase. 94 | * 95 | * @param string $input 96 | * @return string 97 | */ 98 | function camel_case(string $input): string 99 | { 100 | return str_replace('_', '', ucwords($input, '_')); 101 | } 102 | 103 | /** 104 | * Multi-byte alternative for ucfirst(). 105 | * 106 | * @param string $input 107 | * @return string 108 | */ 109 | function mb_ucfirst(string $input): string 110 | { 111 | return mb_strtoupper(mb_substr($input, 0, 1)) . mb_substr($input, 1); 112 | } 113 | 114 | /** 115 | * Multi-byte alternative for lcfirst(). 116 | * 117 | * @param string $input 118 | * @return string 119 | */ 120 | function mb_lcfirst(string $input): string 121 | { 122 | return mb_strtolower(mb_substr($input, 0, 1)) . mb_substr($input, 1); 123 | } 124 | -------------------------------------------------------------------------------- /src/Prelude/ToArray.php: -------------------------------------------------------------------------------- 1 | $value) { 23 | if ($convertCase) { 24 | $key = snake_case($key); 25 | } 26 | 27 | $arr[$key] = $value; 28 | } 29 | 30 | return $arr; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Prelude/ToArrayInterface.php: -------------------------------------------------------------------------------- 1 | values = $values; 43 | } 44 | 45 | /** 46 | * @override 47 | * 48 | * @param string|int $offset 49 | * 50 | * @return bool 51 | */ 52 | public function offsetExists($offset) 53 | { 54 | return isset($this->values[$offset]); 55 | } 56 | 57 | /** 58 | * @override 59 | * 60 | * @param string|int $offset 61 | * 62 | * @return mixed 63 | * @throws OutOfBoundsException 64 | * 65 | */ 66 | public function offsetGet($offset) 67 | { 68 | if (isset($this->values[$offset])) { 69 | return $this->values[$offset]; 70 | } 71 | 72 | throw new OutOfRangeException('Invalid tuple position'); 73 | } 74 | 75 | /** 76 | * @override 77 | * 78 | * @param mixed $offset 79 | * @param mixed $value 80 | * 81 | * @throws RuntimeException 82 | */ 83 | public function offsetSet($offset, $value) 84 | { 85 | throw new RuntimeException('Tuples are immutable!'); 86 | } 87 | 88 | /** 89 | * @override 90 | * 91 | * @param mixed $offset 92 | * 93 | * @throws RuntimeException 94 | */ 95 | public function offsetUnset($offset) 96 | { 97 | throw new RuntimeException('Tuples are immutable!'); 98 | } 99 | 100 | /** 101 | * @return int 102 | */ 103 | public function count() 104 | { 105 | return count($this->values); 106 | } 107 | 108 | /** 109 | * @return array 110 | */ 111 | public function values(): array 112 | { 113 | return $this->values; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Ratchet/GraphQLSubscriptionsConnection.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 29 | $this->key = $key; 30 | } 31 | 32 | /** 33 | * @param string $data 34 | */ 35 | public function send(string $data): void 36 | { 37 | $this->conn->send($data); 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function key(): string 44 | { 45 | return $this->key; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Ratchet/GraphQLSubscriptionsServer.php: -------------------------------------------------------------------------------- 1 | */ 24 | private $connections; 25 | 26 | /** 27 | * SubscriptionsServer constructor. 28 | * 29 | * @param SubscriptionsManager $manager 30 | */ 31 | public function __construct(SubscriptionsManager $manager) 32 | { 33 | $this->manager = $manager; 34 | /** @var SplObjectStorage connections */ 35 | $this->connections = new SplObjectStorage(); 36 | } 37 | 38 | /** 39 | * @override 40 | * @param ConnectionInterface $conn 41 | * @return void 42 | */ 43 | public function onOpen(ConnectionInterface $conn): void 44 | { 45 | $this->connections->offsetSet($conn, new GraphQLSubscriptionsConnection($conn, uniqid())); 46 | } 47 | 48 | /** 49 | * @override 50 | * @param ConnectionInterface $from 51 | * @param string $msg 52 | * @return void 53 | * @throws Exception 54 | */ 55 | public function onMessage(ConnectionInterface $from, $msg): void 56 | { 57 | $from = $this->connections->offsetGet($from); 58 | /** @var array $msg */ 59 | $msg = Json\decode(strval($msg)); 60 | $this->manager->handle($from, $msg); 61 | } 62 | 63 | /** 64 | * @override 65 | * @param ConnectionInterface $conn * 66 | * @return void 67 | */ 68 | public function onClose(ConnectionInterface $conn): void 69 | { 70 | $this->connections->offsetUnset($conn); 71 | } 72 | 73 | /** 74 | * @override 75 | * @param ConnectionInterface $conn 76 | * @param Exception $e 77 | * @return void 78 | */ 79 | public function onError(ConnectionInterface $conn, Exception $e): void 80 | { 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | public function getSubProtocols(): array 87 | { 88 | return [WEBSOCKET_SUB_PROTOCOL]; 89 | } 90 | 91 | /** 92 | * @param ConnectionInterface $conn 93 | * @return SubscriptionsConnection 94 | */ 95 | public function getSubscriptionsConnection(ConnectionInterface $conn): SubscriptionsConnection 96 | { 97 | return $this->connections->offsetGet($conn); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Ratchet/Ratchet.php: -------------------------------------------------------------------------------- 1 | handler = $handler; 29 | $this->pathParams = $pathParams; 30 | } 31 | 32 | /** 33 | * {@inheritdoc}. 34 | */ 35 | public function handle(ServerRequestInterface $request): ResponseInterface 36 | { 37 | $handler = $this->handler; 38 | return $handler($request, $this->pathParams); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Swoole/GraphQLSubscriptionsConnection.php: -------------------------------------------------------------------------------- 1 | fd = $fd; 34 | } 35 | 36 | /** 37 | * @param string $data 38 | */ 39 | public function send(string $data): void 40 | { 41 | push($data, $this->fd); 42 | } 43 | 44 | /** 45 | * @return int 46 | */ 47 | public function key(): int 48 | { 49 | return $this->fd; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Twig/Twig.php: -------------------------------------------------------------------------------- 1 | $debug, 31 | 'cache' => $templatesCachePath 32 | ] 33 | ); 34 | 35 | Container\set('twig', $twig); 36 | 37 | return $twig; 38 | } 39 | 40 | /** 41 | * Renders the given template within the given data. 42 | * 43 | * @param string $name The template name in the templates path 44 | * @param array $data The array of data to used within the template 45 | * 46 | * @return string 47 | * 48 | * @throws LoaderError 49 | * @throws RuntimeError 50 | * @throws SyntaxError 51 | */ 52 | function render(string $name, array $data = []): string 53 | { 54 | /** @var Environment|null $twig */ 55 | $twig = Container\get('twig'); 56 | 57 | if ($twig === null) { 58 | throw new UnexpectedValueException('Twig should be initialized first'); 59 | } 60 | 61 | return $twig->render($name, $data); 62 | } 63 | -------------------------------------------------------------------------------- /tests/Integration/ComposabilityTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($isDivisibleBy3(3)); 21 | $this->assertFalse($isDivisibleBy3(2)); 22 | } 23 | 24 | public function testAny() 25 | { 26 | $isDivisibleBy3 = compose([equal(0), mod(3)]); 27 | $isDivisibleBy5 = compose([equal(0), mod(5)]); 28 | $isDivisibleBy3Or5 = any([$isDivisibleBy3, $isDivisibleBy5]); 29 | 30 | $this->assertFalse($isDivisibleBy3Or5(2)); 31 | $this->assertTrue($isDivisibleBy3Or5(3)); 32 | $this->assertFalse($isDivisibleBy3Or5(4)); 33 | $this->assertTrue($isDivisibleBy3Or5(5)); 34 | } 35 | 36 | public function testAll() 37 | { 38 | $isDivisibleBy3 = compose([equal(0), mod(3)]); 39 | $isDivisibleBy5 = compose([equal(0), mod(5)]); 40 | $isDivisibleBy3And5 = all([$isDivisibleBy3, $isDivisibleBy5]); 41 | 42 | $this->assertFalse($isDivisibleBy3And5(2)); 43 | $this->assertFalse($isDivisibleBy3And5(3)); 44 | $this->assertFalse($isDivisibleBy3And5(4)); 45 | $this->assertFalse($isDivisibleBy3And5(5)); 46 | $this->assertTrue($isDivisibleBy3And5(15)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Integration/FizzbuzzTest.php: -------------------------------------------------------------------------------- 1 | assertSame($expected, $actual); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Integration/RoutingTest.php: -------------------------------------------------------------------------------- 1 | expectOutputString('Hello World'); 24 | Route\get('/', F\puts('Hello World')); 25 | } 26 | 27 | public function testStaticPages() 28 | { 29 | $this->expectOutputString('

Hello World

'); 30 | 31 | Twig\init(__DIR__ . '/../fixtures'); 32 | 33 | Route\get('/', F\puts(Twig\render('static.twig'))); 34 | } 35 | 36 | public function testDynamicPages() 37 | { 38 | $this->expectOutputString('

hello-world

'); 39 | 40 | Twig\init(__DIR__ . '/../fixtures'); 41 | 42 | $_SERVER['REQUEST_URI'] = '/hello-world'; 43 | 44 | Route\get('/{foo}', function ($params) { 45 | echo Twig\render('template.twig', $params); 46 | }); 47 | } 48 | 49 | public function testFiles() 50 | { 51 | $this->expectOutputString('index.get'); 52 | 53 | $_SERVER['REQUEST_URI'] = '/'; 54 | 55 | Route\files('tests/fixtures/route_files/'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Config/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 'bar']); 21 | $processors = [$token]; 22 | 23 | $readers = ['ext' => new Json()]; 24 | 25 | processors($processors); 26 | readers($readers); 27 | 28 | $this->config = load(__DIR__ . '/../../fixtures/config'); 29 | } 30 | 31 | public function testLoad(): void 32 | { 33 | self::assertSame($this->config, Container\get(CONFIG)); 34 | } 35 | 36 | public function testConfig(): void 37 | { 38 | self::assertSame('value', config('test.config')); 39 | self::assertSame('another_value', config('test.another_config')); 40 | } 41 | 42 | public function testConfigDefault(): void 43 | { 44 | self::assertNull(config('invalid_key')); 45 | self::assertSame(123, config('invalid.key', 123)); 46 | } 47 | 48 | public function testHas(): void 49 | { 50 | self::assertTrue(has('test.config')); 51 | self::assertFalse(has('invalid.key')); 52 | } 53 | 54 | public function testAll(): void 55 | { 56 | self::assertSame([ 57 | 'test' => [ 58 | 'json' => 'custom', 59 | 'another_config' => 'another_value', 60 | 'token_processing' => 'bar', 61 | 'config' => 'value' 62 | ] 63 | ], all()); 64 | } 65 | 66 | public function testNoConfig(): void 67 | { 68 | Container\set(CONFIG, null); 69 | self::assertNull(config('test.config')); 70 | self::assertFalse(has('test.config')); 71 | self::assertNull(all()); 72 | } 73 | 74 | public function testYaml(): void 75 | { 76 | yaml(always(['yaml', 'reader', 'test'])); 77 | load(__DIR__ . '/../../fixtures/config/yaml'); 78 | self::assertSame(['yaml', 'reader', 'test'], all()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Unit/Diactoros/DiactorosTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(RequestInterface::class, Diactoros\request()); 22 | } 23 | 24 | public function testResponse() 25 | { 26 | $this->assertInstanceOf(ResponseInterface::class, Diactoros\response()); 27 | } 28 | 29 | public function testHtml() 30 | { 31 | $this->assertInstanceOf(HtmlResponse::class, Diactoros\html('test')); 32 | } 33 | 34 | public function testJson() 35 | { 36 | $this->assertInstanceOf(JsonResponse::class, Diactoros\json('test')); 37 | } 38 | 39 | public function testText() 40 | { 41 | $this->assertInstanceOf(TextResponse::class, Diactoros\text('test')); 42 | } 43 | 44 | public function testNone() 45 | { 46 | $this->assertInstanceOf(EmptyResponse::class, Diactoros\none()); 47 | } 48 | 49 | public function testRedirect() 50 | { 51 | $this->assertInstanceOf(RedirectResponse::class, Diactoros\redirect('test')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Unit/Dotenv/DotenvTest.php: -------------------------------------------------------------------------------- 1 | assertCount(11, $entries); 22 | $this->assertArrayHasKey('FOO', $entries); 23 | $this->assertSame('bar', $entries['FOO']); 24 | $this->assertSame($_SERVER, env()); 25 | $this->assertSame('bar', env('FOO')); 26 | $this->assertSame('baz', env('BAR', 'baz')); 27 | $this->assertNull(env('BAR')); 28 | } 29 | 30 | public function testEvnInt() 31 | { 32 | init(__DIR__ . '/../../fixtures'); 33 | 34 | $this->assertSame(8, int_val('ENV_INT')); 35 | $this->assertNull(int_val('ENV_INT_NULL')); 36 | $this->assertSame(0, int_val('ENV_INT_DEFAULT', 0)); 37 | $this->assertNull(int_val('ENV_INT_NOT_NUMERIC')); 38 | } 39 | 40 | public function testEnvBool() 41 | { 42 | init(__DIR__ . '/../../fixtures'); 43 | 44 | $this->assertNull(bool_val('ENV_BOOL_NULL')); 45 | 46 | foreach (range(0, 5) as $index) { 47 | $this->assertFalse(bool_val("ENV_BOOL_FV$index"), "$index isn't falsy"); 48 | } 49 | 50 | $this->assertTrue(bool_val('ENV_BOOL_TV')); 51 | } 52 | 53 | public function testEnvRequires() 54 | { 55 | $this->expectException(UnexpectedValueException::class); 56 | $this->expectExceptionMessage('BAR is not set in the environment variables'); 57 | 58 | init(__DIR__ . '/../../fixtures'); 59 | 60 | requires('FOO'); 61 | requires('BAR'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Unit/Encoder/JsonTest.php: -------------------------------------------------------------------------------- 1 | assertSame('{"foo":"bar","baz":8}', Json\encode(['foo' => 'bar', 'baz' => 8])); 16 | } 17 | 18 | public function testDecode() 19 | { 20 | $this->assertSame(['foo' => 'bar', 'baz' => 8], Json\decode('{"foo":"bar","baz":8}')); 21 | } 22 | 23 | public function testDecodeException() 24 | { 25 | // TODO: Use JsonException when PHP 7.2 support drops 26 | $this->expectException(Exception::class); 27 | Json\decode('invalid json'); 28 | } 29 | 30 | public function testEncodeException() 31 | { 32 | // TODO: Use JsonException when PHP 7.2 support drops 33 | $this->expectException(Exception::class); 34 | Json\encode(fopen('php://input', 'r')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Env/EnvTest.php: -------------------------------------------------------------------------------- 1 | assertSame('bar', env_var('FOO')); 18 | } 19 | 20 | public function testEnvVarDefault() 21 | { 22 | $this->assertSame('foo', env_var('BAR', 'foo')); 23 | } 24 | 25 | public function testEnvVarThrows() 26 | { 27 | $this->expectException(UnexpectedValueException::class); 28 | env_var('BAR'); 29 | } 30 | 31 | public function testEnvInt() 32 | { 33 | $_ENV['FOO'] = '42'; 34 | $this->assertSame(42, env_int('FOO')); 35 | } 36 | 37 | public function testEnvIntDefault() 38 | { 39 | $this->assertSame(43, env_int('BAR', 43)); 40 | } 41 | 42 | public function testEnvBool() 43 | { 44 | $_ENV['FOO'] = 'true'; 45 | $this->assertSame(true, env_bool('FOO')); 46 | 47 | $_ENV['FOO'] = 'false'; 48 | $this->assertSame(false, env_bool('FOO')); 49 | } 50 | 51 | public function testEnvHas() 52 | { 53 | $_ENV['FOO'] = 'bar'; 54 | $this->assertTrue(env_has('FOO')); 55 | $this->assertFalse(env_has('BAR')); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/File/FileTest.php: -------------------------------------------------------------------------------- 1 | getPathname(); 21 | }, $dir); 22 | 23 | $this->assertContains(join_dir($basedir, 'fixtures', 'foo.php'), $dir); 24 | } 25 | 26 | public function testRecursivelyIteratedDirectoryWithPattern() 27 | { 28 | $basedir = dirname(__DIR__, 2); 29 | $dir = recur_iter_dir(join_dir($basedir, 'fixtures'), '/\.txt$/'); 30 | 31 | $dir = array_map(function (SplFileInfo $info): string { 32 | return $info->getPathname(); 33 | }, $dir); 34 | 35 | $this->assertContains(join_dir($basedir, 'fixtures', 'php_input.txt'), $dir); 36 | } 37 | 38 | public function testConcatFilesWithRecur() 39 | { 40 | $basedir = dirname(__DIR__, 2); 41 | $result = concat_files(recur_iter_dir(join_dir($basedir, 'fixtures', 'concat')), ''); 42 | 43 | // Note: file order is arbitrary 44 | $this->assertTrue($result === 'foobar' || $result === 'barfoo'); 45 | } 46 | 47 | public function testConcatFiles() 48 | { 49 | $result = concat_files([__DIR__ . '/../../fixtures/concat/foo.txt', __DIR__ . '/../../fixtures/concat/bar.txt']); 50 | $this->assertSame("foo\nbar", $result); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Unit/Functional/MonadTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Monad\Identity::class, $monad); 17 | $this->assertSame(1, $monad()); 18 | 19 | $monad = $monad(f\add(1)); 20 | $this->assertInstanceOf(Monad\Identity::class, $monad); 21 | $this->assertSame(2, $monad()); 22 | } 23 | 24 | public function testMaybe() 25 | { 26 | $maybe = Monad\maybe(1); 27 | $this->assertInstanceOf(Monad\Maybe::class, $maybe); 28 | $this->assertSame(1, $maybe()); 29 | 30 | $maybe = $maybe(f\add(1)); 31 | $this->assertInstanceOf(Monad\Maybe::class, $maybe); 32 | $this->assertSame(2, $maybe()); 33 | 34 | $maybe = $maybe(f\always(null)); 35 | $this->assertInstanceOf(Monad\Maybe::class, $maybe); 36 | 37 | $maybe = $maybe(f\mul(2)); 38 | $this->assertNull($maybe()); 39 | } 40 | 41 | public function testMaybeTree() 42 | { 43 | $foo = [ 44 | 'name' => 'foo', 45 | 'parent' => null 46 | ]; 47 | $bar = [ 48 | 'name' => 'bar', 49 | 'parent' => $foo 50 | ]; 51 | $baz = [ 52 | 'name' => 'baz', 53 | 'parent' => $bar 54 | ]; 55 | 56 | $parent = function ($value) { 57 | return $value['parent']; 58 | }; 59 | 60 | $grandparent = Monad\maybe($baz)($parent)($parent); 61 | $this->assertSame($foo, $grandparent()); 62 | 63 | $grandparent = Monad\maybe($foo)($parent)($parent); 64 | $this->assertNull($grandparent()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Unit/GraphQL/Annotated/Bar.php: -------------------------------------------------------------------------------- 1 | x + $input->y; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/GraphQL/Annotated/MyDirective.php: -------------------------------------------------------------------------------- 1 | name('baz') 39 | ->type('string') 40 | ->resolve(always('Baz')) 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/GraphQL/AnnotatedTest.php: -------------------------------------------------------------------------------- 1 | assertValid(); 30 | 31 | $this->assertTrue($schema->hasType('Query')); 32 | $this->assertTrue($schema->hasType('Mutation')); 33 | 34 | $result = execute($schema, ['query' => 'query { hello }']); 35 | $this->assertSame(['data' => ['hello' => 'world']], $result); 36 | 37 | $result = execute($schema, ['query' => 'mutation { sum(input: {x: 2, y: 2}) }']); 38 | $this->assertSame(['data' => ['sum' => 4]], $result); 39 | 40 | $result = execute($schema, ['query' => 'query { foo { enum foo } }']); 41 | $this->assertSame(['data' => ['foo' => ['enum' => Enum::YES, 'foo' => 'foo']]], $result); 42 | 43 | $result = execute($schema, ['query' => 'query { bar { myBool myFloat } }']); 44 | $this->assertSame(['data' => ['bar' => ['myBool' => true, 'myFloat' => 4.2]]], $result); 45 | 46 | $this->assertNotNull($schema->getDirective('myDirective')); 47 | 48 | $result = execute($schema, ['query' => 'query { baz }']); 49 | $this->assertSame(['data' => ['baz' => 'Baz']], $result); 50 | } 51 | 52 | public function testListOfException() 53 | { 54 | $this->expectException(TypeError::class); 55 | annotated(ListOfException::class); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/GraphQL/DateTimeScalarTest.php: -------------------------------------------------------------------------------- 1 | serialize(DateTime::createFromFormat(DateScalar::FORMAT, $expected)); 21 | self::assertSame($expected, $actual); 22 | 23 | $this->expectException(Error::class); 24 | $ds->serialize($expected); 25 | } 26 | 27 | public function testDateParse(): void 28 | { 29 | $value = '2020-07-18'; 30 | $literal = new StringValueNode(['value' => $value]); 31 | 32 | $expected = DateTime::createFromFormat(DateScalar::FORMAT, $value); 33 | $ds = new DateScalar(); 34 | 35 | $actual = $ds->parseLiteral($literal); 36 | self::assertEquals($expected, $actual); 37 | 38 | $this->expectException(Error::class); 39 | $ds->parseLiteral(new IntValueNode(['value' => 0])); 40 | 41 | $actual = $ds->parseValue($value); 42 | self::assertEquals($expected, $actual); 43 | 44 | $this->expectException(Error::class); 45 | $ds->parseValue('2020-07-18'); 46 | } 47 | 48 | public function testDateTimeSerialize(): void 49 | { 50 | $expected = '2020-07-18 13:40:00'; 51 | $dts = new DateTimeScalar(); 52 | 53 | $actual = $dts->serialize(DateTime::createFromFormat(DateTimeScalar::FORMAT, $expected)); 54 | self::assertSame($expected, $actual); 55 | 56 | $this->expectException(Error::class); 57 | $dts->serialize($expected); 58 | } 59 | 60 | public function testDateTimeParse(): void 61 | { 62 | $value = '2020-07-18 13:40:00'; 63 | $literal = new StringValueNode(['value' => $value]); 64 | 65 | $expected = DateTime::createFromFormat(DateTimeScalar::FORMAT, $value); 66 | $dts = new DateTimeScalar(); 67 | 68 | $actual = $dts->parseLiteral($literal); 69 | self::assertEquals($expected, $actual); 70 | 71 | $this->expectException(Error::class); 72 | $dts->parseLiteral(new IntValueNode(['value' => 0])); 73 | 74 | $actual = $dts->parseValue($value); 75 | self::assertEquals($expected, $actual); 76 | 77 | $this->expectException(Error::class); 78 | $dts->parseValue('2020-07-18'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Unit/HttpHandlerRunner/HttpHandlerRunnerTest.php: -------------------------------------------------------------------------------- 1 | 'bar']); 19 | 20 | $this->expectOutputString('{"foo":"bar"}'); 21 | 22 | $result = HttpHandlerRunner\sapi_emit($response); 23 | 24 | $this->assertTrue($result); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/Mail/SwiftMailerTest.php: -------------------------------------------------------------------------------- 1 | expectException(UnderflowException::class); 21 | Mail\send(new Swift_Message()); 22 | } 23 | 24 | public function testMailer() 25 | { 26 | $mailer = Mail\mailer(new Swift_NullTransport()); 27 | $this->assertInstanceOf(Swift_Mailer::class, $mailer); 28 | $this->assertSame(Container\get(Mail\SWIFT_MAILER), $mailer); 29 | 30 | Mail\send(new Swift_Message()); 31 | } 32 | 33 | public function testSugar() 34 | { 35 | $message = Mail\message('subject', ['from@from.from'], ['to@to.to'], 'body'); 36 | $this->assertInstanceOf(Swift_Message::class, $message); 37 | $this->assertSame('subject', $message->getSubject()); 38 | $this->assertSame('body', $message->getBody()); 39 | $this->assertSame('text/plain', $message->getBodyContentType()); 40 | $this->assertSame(['from@from.from' => null], $message->getFrom()); 41 | $this->assertSame(['to@to.to' => null], $message->getTo()); 42 | 43 | $smtp = Mail\smtp('host', 0, 'username', 'password'); 44 | $this->assertInstanceOf(Swift_SmtpTransport::class, $smtp); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/Monolog/MonologTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(StreamHandler::class, $handler); 20 | } 21 | 22 | public function testLog() 23 | { 24 | $handler = new TestHandler(); 25 | 26 | Log\handler($handler); 27 | Log\log(Logger::WARNING, 'test'); 28 | 29 | list($record) = $handler->getRecords(); 30 | 31 | $this->assertSame(Log\MONOLOG_DEFAULT_CHANNEL, $record['channel']); 32 | } 33 | 34 | public function testSugar() 35 | { 36 | $handler = new TestHandler(); 37 | Log\handler($handler, 'test'); 38 | 39 | $levels = array_values(Logger::getLevels()); 40 | 41 | foreach ($levels as $level) { 42 | $levelName = strtolower(Logger::getLevelName($level)); 43 | call_user_func("Siler\\Monolog\\$levelName", $levelName, ['context' => $levelName], 'test'); 44 | } 45 | 46 | $records = $handler->getRecords(); 47 | 48 | foreach ($levels as $i => $level) { 49 | $levelName = Logger::getLevelName($level); 50 | $record = $records[$i]; 51 | 52 | $this->assertSame(strtolower($levelName), $record['message']); 53 | $this->assertSame($level, $record['level']); 54 | $this->assertSame($levelName, $record['level_name']); 55 | $this->assertArrayHasKey('context', $record['context']); 56 | $this->assertSame(strtolower($levelName), $record['context']['context']); 57 | $this->assertSame('test', $record['channel']); 58 | } 59 | } 60 | 61 | public function testLogIf() 62 | { 63 | $handler = new TestHandler(); 64 | $channel = 'log_if'; 65 | 66 | Log\handler($handler, $channel); 67 | Log\debug('foo', [], $channel); 68 | Log\debug_if(always(false)); 69 | Log\debug('bar', [], $channel); 70 | 71 | $records = $handler->getRecords(); 72 | 73 | $this->assertCount(1, $records); 74 | $this->assertSame('foo', $records[0]['message']); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/ArrTest.php: -------------------------------------------------------------------------------- 1 | ['bar' => 'baz']]; 14 | $expected = ['foo' => ['bar' => 'qux']]; 15 | 16 | set($fixture, 'foo.bar', 'qux'); 17 | 18 | $this->assertSame($expected, $fixture); 19 | } 20 | 21 | public function testSetDeeplyCreates() 22 | { 23 | $fixture = ['foo' => []]; 24 | $expected = ['foo' => ['bar' => 'qux']]; 25 | 26 | set($fixture, 'foo.bar', 'qux'); 27 | 28 | $this->assertSame($expected, $fixture); 29 | } 30 | 31 | public function testAssoc() 32 | { 33 | $fixture = [ 34 | ['foo', 'bar'], 35 | ['baz', 'qux'], 36 | [4, 2], 37 | ]; 38 | 39 | $expected = [ 40 | ['foo' => 'baz', 'bar' => 'qux'], 41 | ['foo' => 4, 'bar' => 2], 42 | ]; 43 | 44 | $actual = assoc($fixture); 45 | $this->assertSame($expected, $actual); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/CollectionTest.php: -------------------------------------------------------------------------------- 1 | filter(even)->equals(collect([2, 4]))); 18 | } 19 | 20 | public function testToArray(): void 21 | { 22 | self::assertSame([1, 2, 3], collect([1, 2, 3])->toArray()); 23 | } 24 | 25 | public function testFold(): void 26 | { 27 | self::assertSame(6, collect([1, 2, 3])->fold(0, sum)); 28 | } 29 | 30 | public function testMap(): void 31 | { 32 | self::assertTrue(collect([1, 2, 3])->map(mul(2))->equals(collect([2, 4, 6]))); 33 | } 34 | 35 | public function testMerge(): void 36 | { 37 | self::assertTrue(collect([1, 2, 3, 4, 5, 6])->equals(collect([1, 2, 3])->merge(collect([4, 5, 6])))); 38 | } 39 | 40 | public function testJoin(): void 41 | { 42 | self::assertSame('123', collect([1, 2, 3])->join()); 43 | self::assertSame('1,2,3', collect([1, 2, 3])->join(',')); 44 | } 45 | 46 | public function testFirst(): void 47 | { 48 | self::assertSame(1, collect([1, 2 ,3])->first()); 49 | self::assertNull(collect([])->first()); 50 | } 51 | 52 | public function testLast(): void 53 | { 54 | self::assertSame(3, collect([1, 2 ,3])->last()); 55 | self::assertNull(collect([])->last()); 56 | } 57 | 58 | public function testIsEmpty(): void 59 | { 60 | self::assertTrue(collect([])->isEmpty()); 61 | self::assertFalse(collect([1, 2, 3])->isEmpty()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/DispatcherTest.php: -------------------------------------------------------------------------------- 1 | listen(TestEvent::class, function (TestEvent $event) { 16 | $this->assertSame('test', $event->payload); 17 | }); 18 | 19 | $dispatcher->dispatch(new TestEvent('test')); 20 | } 21 | 22 | public function testGetListenersForEvent() 23 | { 24 | $dispatcher = new Dispatcher(); 25 | 26 | $callback = function (TestEvent $event) { 27 | }; 28 | 29 | $dispatcher->listen(TestEvent::class, $callback); 30 | 31 | $this->assertSame([$callback], $dispatcher->getListenersForEvent(new TestEvent('test'))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/EnumTest.php: -------------------------------------------------------------------------------- 1 | assertSame(0, $foo->valueOf()); 13 | 14 | $another_foo = TestEnum::of(TestEnum::FOO); 15 | $this->assertTrue($foo->sameAs($another_foo)); 16 | 17 | $bar = TestEnum::of(TestEnum::BAR); 18 | $this->assertFalse($foo->sameAs($bar)); 19 | 20 | $this->expectException(\UnexpectedValueException::class); 21 | $this->expectExceptionMessage(sprintf('Invalid value (foo) for enum (%s)', TestEnum::class)); 22 | TestEnum::of('foo'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/FromToArrayTest.php: -------------------------------------------------------------------------------- 1 | 'foo', 17 | 'fooBar' => 'bar', 18 | 'foo_bar_baz' => 'baz', 19 | ]); 20 | 21 | $this->assertSame('foo', $fixture->foo); 22 | $this->assertSame('bar', $fixture->fooBar); 23 | $this->assertSame('baz', $fixture->fooBarBaz); 24 | } 25 | 26 | public function testToArray() 27 | { 28 | $fixture = new PatchFromToFixture(); 29 | $fixture->foo = 'foo'; 30 | $fixture->fooBar = 'bar'; 31 | 32 | $arr = $fixture->toArray(); 33 | $this->assertSame('foo', $arr['foo']); 34 | $this->assertSame('bar', $arr['foo_bar']); 35 | $this->assertNull($arr['foo_bar_baz'] ?? null); 36 | $this->assertNull($arr['fooBarBaz'] ?? null); 37 | 38 | $arr = $fixture->toArray(false); 39 | $this->assertSame('foo', $arr['foo']); 40 | $this->assertSame('bar', $arr['fooBar']); 41 | $this->assertNull($arr['foo_bar'] ?? null); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/IOTest.php: -------------------------------------------------------------------------------- 1 | expectOutputString('foo' . PHP_EOL); 15 | println('foo'); 16 | } 17 | 18 | public function testCsvToArray() 19 | { 20 | $arr = csv_to_array(__DIR__ . '/../../fixtures/test.csv'); 21 | $this->assertSame([['1', 'foo'], ['2', 'bar'], ['3', 'baz']], $arr); 22 | } 23 | 24 | public function testFetch() 25 | { 26 | $args = ['foo' => 'bar']; 27 | ['response' => $response, 'error' => $err, 'status' => $code] = fetch('https://postman-echo.com/get', ['query' => $args]); 28 | $this->assertNull($err); 29 | $this->assertSame(200, $code); 30 | $this->assertSame($args, $response['args']); 31 | 32 | ['status' => $code, 'error' => $err] = fetch('https://postman-echo.com/status/418'); 33 | $this->assertNull($err); 34 | $this->assertSame(418, $code); 35 | 36 | ['response' => $response, 'error' => $err, 'status' => $code] = fetch('https://postman-echo.com/post', [ 37 | 'method' => 'post', 38 | 'body' => 'foobar', 39 | 'headers' => ['content-type' => 'text/plain'], 40 | ]); 41 | $this->assertNull($err); 42 | $this->assertSame(200, $code); 43 | $this->assertSame('foobar', $response['data']); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/ObjTest.php: -------------------------------------------------------------------------------- 1 | foo = 'bar'; 13 | 14 | $return_ref = $target->patch(['foo' => 'baz']); 15 | 16 | $this->assertSame($return_ref, $target); 17 | $this->assertSame('baz', $target->foo); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/PatchFromToFixture.php: -------------------------------------------------------------------------------- 1 | 14 | * @implements PatchInterface 15 | */ 16 | class PatchFromToFixture implements FromArrayInterface, ToArrayInterface, PatchInterface 17 | { 18 | /** @use FromArray */ 19 | use FromArray; 20 | use ToArray; 21 | 22 | /** @use Patch */ 23 | use Patch; 24 | 25 | public $foo; 26 | public $fooBar; 27 | public $fooBarBaz; 28 | } 29 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/Test.php: -------------------------------------------------------------------------------- 1 | assertSame('KlassTest', unqualified_name(KlassTest::class)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Unit/Prelude/TestEnum.php: -------------------------------------------------------------------------------- 1 | assertFalse(is_array($tuple)); 19 | $this->assertSame(1, $tuple[0]); 20 | $this->assertSame('a', $tuple[1]); 21 | $this->assertSame(true, $tuple[2]); 22 | $this->assertTrue(isset($tuple[1])); 23 | $this->assertIsArray($tuple->values()); 24 | $this->assertSame([1, 'a', true], $tuple->values()); 25 | } 26 | 27 | public function testOutOfRangeGet() 28 | { 29 | $this->expectException(OutOfRangeException::class); 30 | $tuple = tuple(1); 31 | $tuple[1]; 32 | } 33 | 34 | public function testImmutableSet() 35 | { 36 | $this->expectException(RuntimeException::class); 37 | $tuple = tuple(1); 38 | $tuple[1] = 2; 39 | } 40 | 41 | public function testImmutableUnset() 42 | { 43 | $this->expectException(RuntimeException::class); 44 | $tuple = tuple(1); 45 | unset($tuple[0]); 46 | } 47 | 48 | public function testCount() 49 | { 50 | $tuple = tuple(1, 2, 3); 51 | $this->assertCount(3, $tuple); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Unit/Ratchet/GraphQLSubscriptionsConnectionTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(ConnectionInterface::class)->getMock(); 16 | $conn = new GraphQLSubscriptionsConnection($interface, 'test_key'); 17 | $this->assertSame('test_key', $conn->key()); 18 | } 19 | 20 | public function testSend() 21 | { 22 | $interface = $this->getMockBuilder(ConnectionInterface::class)->getMock(); 23 | $interface->expects($this->once()) 24 | ->method('send') 25 | ->with('test_data'); 26 | 27 | $conn = new GraphQLSubscriptionsConnection($interface, 'test_key'); 28 | $conn->send('test_data'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/Ratchet/RatchetTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(SubscriptionsManager::class) 15 | ->disableOriginalConstructor() 16 | ->getMock(); 17 | 18 | $server = graphql_subscriptions($manager); 19 | $this->assertInstanceOf(IoServer::class, $server); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Route/RouteClass.php: -------------------------------------------------------------------------------- 1 | expectOutputString('className.index'); 13 | $_SERVER['REQUEST_METHOD'] = 'GET'; 14 | $_SERVER['REQUEST_URI'] = '/class-name'; 15 | Route\class_name('/class-name', 'Siler\Test\Unit\Route\RouteClass'); 16 | } 17 | 18 | /** 19 | * @runInSeparateProcess 20 | * @preserveGlobalState disabled 21 | */ 22 | public function testPostFoo() 23 | { 24 | $this->expectOutputString('className.postFoo'); 25 | $_SERVER['REQUEST_METHOD'] = 'POST'; 26 | $_SERVER['REQUEST_URI'] = '/class-name/foo'; 27 | Route\class_name('/class-name', 'Siler\Test\Unit\Route\RouteClass'); 28 | } 29 | 30 | /** 31 | * @runInSeparateProcess 32 | * @preserveGlobalState disabled 33 | */ 34 | public function testPutFooBar() 35 | { 36 | $this->expectOutputString('className.putFooBar'); 37 | $_SERVER['REQUEST_METHOD'] = 'PUT'; 38 | $_SERVER['REQUEST_URI'] = '/class-name/foo/bar'; 39 | Route\class_name('/class-name', 'Siler\Test\Unit\Route\RouteClass'); 40 | } 41 | 42 | public function testAnyParams() 43 | { 44 | $this->expectOutputString('className.baz.qux'); 45 | $_SERVER['REQUEST_METHOD'] = 'ANYTHING'; 46 | $_SERVER['REQUEST_URI'] = '/class-name/baz/qux'; 47 | Route\class_name('/class-name', 'Siler\Test\Unit\Route\RouteClass'); 48 | } 49 | 50 | public function tearDown(): void 51 | { 52 | Route\resume(); 53 | } 54 | 55 | protected function setUp(): void 56 | { 57 | Route\purge_match(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Unit/Route/RouteFacadeTest.php: -------------------------------------------------------------------------------- 1 | expectException(Exception::class); 16 | $this->expectExceptionMessage('Route /bar/baz should match'); 17 | 18 | $_SERVER['REQUEST_METHOD'] = 'GET'; 19 | 20 | Route\get('/bar/baz', function ($params) { 21 | throw new Exception('Route /bar/baz should match'); 22 | }); 23 | } 24 | 25 | public function testPost() 26 | { 27 | $this->expectException(Exception::class); 28 | $this->expectExceptionMessage('Route /bar/baz should match'); 29 | 30 | $_SERVER['REQUEST_METHOD'] = 'POST'; 31 | 32 | Route\post('/bar/baz', function ($params) { 33 | throw new Exception('Route /bar/baz should match'); 34 | }); 35 | } 36 | 37 | public function testPut() 38 | { 39 | $this->expectException(Exception::class); 40 | $this->expectExceptionMessage('Route /bar/baz should match'); 41 | 42 | $_SERVER['REQUEST_METHOD'] = 'PUT'; 43 | 44 | Route\put('/bar/baz', function ($params) { 45 | throw new Exception('Route /bar/baz should match'); 46 | }); 47 | } 48 | 49 | public function testDelete() 50 | { 51 | $this->expectException(Exception::class); 52 | $this->expectExceptionMessage('Route /bar/baz should match'); 53 | 54 | $_SERVER['REQUEST_METHOD'] = 'DELETE'; 55 | 56 | Route\delete('/bar/baz', function ($params) { 57 | throw new Exception('Route /bar/baz should match'); 58 | }); 59 | } 60 | 61 | public function testOptions() 62 | { 63 | $this->expectException(Exception::class); 64 | $this->expectExceptionMessage('Route /bar/baz should match'); 65 | 66 | $_SERVER['REQUEST_METHOD'] = 'OPTIONS'; 67 | 68 | Route\options('/bar/baz', function ($params) { 69 | throw new Exception('Route /bar/baz should match'); 70 | }); 71 | } 72 | 73 | public function testAny() 74 | { 75 | $this->expectException(Exception::class); 76 | $this->expectExceptionMessage('Route /bar/baz should match'); 77 | 78 | $_SERVER['REQUEST_METHOD'] = 'ANYTHING'; 79 | 80 | Route\any('/bar/baz', function ($params) { 81 | throw new Exception('Route /bar/baz should match'); 82 | }); 83 | } 84 | 85 | protected function setUp(): void 86 | { 87 | $_GET = $_POST = $_REQUEST = ['foo' => 'bar']; 88 | 89 | $_SERVER['HTTP_HOST'] = 'test:8000'; 90 | $_SERVER['SCRIPT_NAME'] = '/foo/test.php'; 91 | $_SERVER['PATH_INFO'] = '/bar/baz'; 92 | $_SERVER['REQUEST_URI'] = '/bar/baz'; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Unit/Route/RouteFileTest.php: -------------------------------------------------------------------------------- 1 | expectOutputString('index.get'); 20 | 21 | $_SERVER['REQUEST_METHOD'] = 'GET'; 22 | $_SERVER['REQUEST_URI'] = '/'; 23 | 24 | Route\files(__DIR__ . '/../../fixtures/route_files/'); 25 | } 26 | 27 | /** 28 | * @runInSeparateProcess 29 | * @preserveGlobalState disabled 30 | */ 31 | public function testGetContact() 32 | { 33 | $this->expectOutputString('contact.get'); 34 | 35 | $_SERVER['REQUEST_METHOD'] = 'GET'; 36 | $_SERVER['REQUEST_URI'] = '/contact'; 37 | 38 | Route\files(__DIR__ . '/../../fixtures/route_files/'); 39 | } 40 | 41 | /** 42 | * @runInSeparateProcess 43 | * @preserveGlobalState disabled 44 | */ 45 | public function testPostContact() 46 | { 47 | $this->expectOutputString('contact.post'); 48 | 49 | $_SERVER['REQUEST_METHOD'] = 'POST'; 50 | $_SERVER['REQUEST_URI'] = '/contact'; 51 | 52 | Route\files(__DIR__ . '/../../fixtures/route_files/'); 53 | } 54 | 55 | /** 56 | * @runInSeparateProcess 57 | * @preserveGlobalState disabled 58 | */ 59 | public function testGetAbout() 60 | { 61 | $this->expectOutputString('about.index.get'); 62 | 63 | $_SERVER['REQUEST_METHOD'] = 'GET'; 64 | $_SERVER['REQUEST_URI'] = '/about'; 65 | 66 | Route\files(__DIR__ . '/../../fixtures/route_files/'); 67 | } 68 | 69 | /** 70 | * @runInSeparateProcess 71 | * @preserveGlobalState disabled 72 | */ 73 | public function testGetWithParam() 74 | { 75 | $this->expectOutputString('foo.$8.get'); 76 | 77 | $_SERVER['SCRIPT_NAME'] = '/index.php'; 78 | $_SERVER['REQUEST_METHOD'] = 'GET'; 79 | $_SERVER['REQUEST_URI'] = '/foo/8'; 80 | 81 | Route\files(__DIR__ . '/../../fixtures/route_files/'); 82 | } 83 | 84 | /** 85 | * @runInSeparateProcess 86 | * @preserveGlobalState disabled 87 | */ 88 | public function testNotExists() 89 | { 90 | $this->expectException(InvalidArgumentException::class); 91 | Route\files('path/does/not/exists'); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Unit/Route/RouteFileWithPrefixTest.php: -------------------------------------------------------------------------------- 1 | expectOutputString('index.get'); 19 | 20 | $_SERVER['REQUEST_METHOD'] = 'GET'; 21 | $_SERVER['REQUEST_URI'] = '/foo/'; 22 | 23 | Route\files(__DIR__ . '/../../fixtures/route_files/', '/foo'); 24 | } 25 | 26 | /** 27 | * @runInSeparateProcess 28 | * @preserveGlobalState disabled 29 | */ 30 | public function testGetContact() 31 | { 32 | $this->expectOutputString('contact.get'); 33 | 34 | $_SERVER['REQUEST_METHOD'] = 'GET'; 35 | $_SERVER['REQUEST_URI'] = '/foo/contact'; 36 | 37 | Route\files(__DIR__ . '/../../fixtures/route_files/', '/foo'); 38 | } 39 | 40 | /** 41 | * @runInSeparateProcess 42 | * @preserveGlobalState disabled 43 | */ 44 | public function testPostContact() 45 | { 46 | $this->expectOutputString('contact.post'); 47 | 48 | $_SERVER['REQUEST_METHOD'] = 'POST'; 49 | $_SERVER['REQUEST_URI'] = '/foo/contact'; 50 | 51 | Route\files(__DIR__ . '/../../fixtures/route_files/', '/foo'); 52 | } 53 | 54 | /** 55 | * @runInSeparateProcess 56 | * @preserveGlobalState disabled 57 | */ 58 | public function testGetAbout() 59 | { 60 | $this->expectOutputString('about.index.get'); 61 | 62 | $_SERVER['REQUEST_METHOD'] = 'GET'; 63 | $_SERVER['REQUEST_URI'] = '/foo/about'; 64 | 65 | Route\files(__DIR__ . '/../../fixtures/route_files/', '/foo'); 66 | } 67 | 68 | /** 69 | * @runInSeparateProcess 70 | * @preserveGlobalState disabled 71 | */ 72 | public function testGetWithParam() 73 | { 74 | $this->expectOutputString('foo.$8.get'); 75 | 76 | $_SERVER['SCRIPT_NAME'] = '/index.php'; 77 | $_SERVER['REQUEST_METHOD'] = 'GET'; 78 | $_SERVER['REQUEST_URI'] = '/foo/foo/8'; 79 | 80 | Route\files(__DIR__ . '/../../fixtures/route_files/', '/foo'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Unit/Route/RoutePsr7Test.php: -------------------------------------------------------------------------------- 1 | '/test']; 20 | $request = ServerRequestFactory::fromGlobals($server); 21 | 22 | $actual = Route\get( 23 | '/test', 24 | function () { 25 | return 'foo'; 26 | }, 27 | $request 28 | ); 29 | 30 | $this->assertSame('foo', $actual); 31 | } 32 | 33 | /** 34 | * @runInSeparateProcess 35 | * @preserveGlobalState disabled 36 | */ 37 | public function testNullRoute() 38 | { 39 | $server = ['REQUEST_URI' => '/foo']; 40 | $request = ServerRequestFactory::fromGlobals($server); 41 | 42 | $actual = Route\get( 43 | '/bar', 44 | function () { 45 | return 'baz'; 46 | }, 47 | $request 48 | ); 49 | 50 | $this->assertNull($actual); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Unit/Route/RouteResourceTest.php: -------------------------------------------------------------------------------- 1 | expectOutputString('resources.index'); 15 | 16 | $_SERVER['REQUEST_METHOD'] = 'GET'; 17 | $_SERVER['REQUEST_URI'] = '/resources'; 18 | 19 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources'); 20 | } 21 | 22 | /** 23 | * @runInSeparateProcess 24 | */ 25 | public function testCreateResource() 26 | { 27 | $this->expectOutputString('resources.create'); 28 | 29 | $_SERVER['REQUEST_METHOD'] = 'GET'; 30 | $_SERVER['REQUEST_URI'] = '/resources/create'; 31 | 32 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources'); 33 | } 34 | 35 | public function testStoreResource() 36 | { 37 | $this->expectOutputString('resources.store bar'); 38 | 39 | $_SERVER['REQUEST_METHOD'] = 'POST'; 40 | $_SERVER['REQUEST_URI'] = '/resources'; 41 | $_POST['foo'] = 'bar'; 42 | 43 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources'); 44 | } 45 | 46 | public function testShowResource() 47 | { 48 | $this->expectOutputString('resources.show 8'); 49 | 50 | $_SERVER['REQUEST_METHOD'] = 'GET'; 51 | $_SERVER['REQUEST_URI'] = '/resources/8'; 52 | 53 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources'); 54 | } 55 | 56 | public function testEditResource() 57 | { 58 | $this->expectOutputString('resources.edit 8'); 59 | 60 | $_SERVER['REQUEST_METHOD'] = 'GET'; 61 | $_SERVER['REQUEST_URI'] = '/resources/8/edit'; 62 | 63 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources'); 64 | } 65 | 66 | public function testUpdateResource() 67 | { 68 | $this->expectOutputString('resources.update 8'); 69 | 70 | $_SERVER['REQUEST_METHOD'] = 'PUT'; 71 | $_SERVER['REQUEST_URI'] = '/resources/8'; 72 | 73 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources'); 74 | } 75 | 76 | public function testDestroyResource() 77 | { 78 | $this->expectOutputString('resources.destroy 8'); 79 | 80 | $_SERVER['REQUEST_METHOD'] = 'DELETE'; 81 | $_SERVER['REQUEST_URI'] = '/resources/8'; 82 | 83 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources'); 84 | } 85 | 86 | /** 87 | * @runInSeparateProcess 88 | */ 89 | public function testOverrideIdentity() 90 | { 91 | $this->expectOutputString('resources.edit foo'); 92 | 93 | $_SERVER['REQUEST_METHOD'] = 'GET'; 94 | $_SERVER['REQUEST_URI'] = '/resources/foo/edit'; 95 | $_SERVER['SCRIPT_NAME'] = '/test/index.php'; 96 | 97 | Route\resource('/resources', __DIR__ . '/../../fixtures/resources/slug', 'slug'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/Unit/Route/RouteStaticMethodTest.php: -------------------------------------------------------------------------------- 1 | assertSame('static_method', $result); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Unit/Route/RouteUtf8Test.php: -------------------------------------------------------------------------------- 1 | expectOutputString('foo'); 17 | $_SERVER['REQUEST_URI'] = rawurlencode('/жолжаксынов'); 18 | Route\get('/жолжаксынов', function () { 19 | echo 'foo'; 20 | }); 21 | } 22 | 23 | public function testUtf8B() 24 | { 25 | $this->expectOutputString('victon-빅톤-mayday'); 26 | $_SERVER['REQUEST_URI'] = rawurlencode('/test/victon-빅톤-mayday'); 27 | Route\get('/test/{test:.*}', function (array $params) { 28 | echo $params['test']; 29 | }); 30 | } 31 | 32 | public function testUtf8C() 33 | { 34 | $this->expectOutputString('আড়-ইহ-জ-র-দ-ড়-বছর-র-শ-শ-র-গল-য়-ছ-র'); 35 | $_SERVER['REQUEST_URI'] = rawurlencode('/foo/আড়-ইহ-জ-র-দ-ড়-বছর-র-শ-শ-র-গল-য়-ছ-র/baz'); 36 | Route\get('/foo/{bar:.*}/baz', function (array $params) { 37 | echo $params['bar']; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/Route/SwooleHttpRequestMock.php: -------------------------------------------------------------------------------- 1 | server = [ 15 | 'request_method' => $method, 16 | 'request_uri' => $uri 17 | ]; 18 | 19 | $this->header = $header; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Stratigility/RequestHandlerDecoratorTest.php: -------------------------------------------------------------------------------- 1 | 'bar']; 22 | 23 | $handler = function ( 24 | ServerRequestInterface $_request, 25 | array $_pathParams 26 | ) use ( 27 | $request, 28 | $response, 29 | $params 30 | ): ResponseInterface { 31 | $this->assertSame($request, $_request); 32 | $this->assertSame($params, $_pathParams); 33 | 34 | return $response; 35 | }; 36 | 37 | $decorator = new RequestHandlerDecorator($handler, $params); 38 | $decorator_response = $decorator->handle($request); 39 | 40 | $this->assertSame($response, $decorator_response); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Unit/Swoole/SwooleTest.php: -------------------------------------------------------------------------------- 1 | assertSame(3, $middleware(new Request(), new Response())); 31 | 32 | $early_return = [ 33 | function (Request $request, Response $response, $value) { 34 | return $request->get['foo'] ?? 'foobar'; 35 | }, 36 | function (Request $request, Response $response, $value) { 37 | if ($value === 'bar') { 38 | return 'baz'; 39 | } 40 | 41 | $response->header = 'quz'; 42 | return null; 43 | }, 44 | function (Request $request, Response $response, $value) { 45 | $response->header = $value; 46 | }, 47 | ]; 48 | 49 | $request = new Request(); 50 | $request->get = ['foo' => 'bar']; 51 | $response = new Response(); 52 | middleware($early_return)($request, $response); 53 | $this->assertSame('baz', $response->header); 54 | 55 | $request = new Request(); 56 | $request->get = []; 57 | $response = new Response(); 58 | middleware($early_return)($request, $response); 59 | $this->assertSame('quz', $response->header); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Unit/Twig/TwigTest.php: -------------------------------------------------------------------------------- 1 | expectException(RuntimeException::class); 18 | $this->expectExceptionMessage('Twig should be initialized first'); 19 | 20 | Container\set('twig', null); 21 | Twig\render('template.twig'); 22 | } 23 | 24 | public function testCreateTwigEnv() 25 | { 26 | $twigEnv = Twig\init(__DIR__ . '/../../fixtures'); 27 | $this->assertInstanceOf(Environment::class, $twigEnv); 28 | } 29 | 30 | public function testRender() 31 | { 32 | Twig\init(__DIR__ . '/../../fixtures'); 33 | $this->assertSame('

bar

', Twig\render('template.twig', ['foo' => 'bar'])); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/fixtures/.env: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | APP_KEY=test 3 | 4 | ENV_INT=8 5 | ENV_INT_NOT_NUMERIC=eight 6 | 7 | ENV_BOOL_FV0=false 8 | ENV_BOOL_FV1='0' 9 | ENV_BOOL_FV2=0 10 | ENV_BOOL_FV3='' 11 | ENV_BOOL_FV4=[] 12 | ENV_BOOL_FV5={} 13 | 14 | ENV_BOOL_TV=true -------------------------------------------------------------------------------- /tests/fixtures/TestEvent.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/fixtures/callable_require.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'config' => 'value' 6 | ] 7 | ]; 8 | -------------------------------------------------------------------------------- /tests/fixtures/config/yaml/test.yml: -------------------------------------------------------------------------------- 1 | foo: 2 | bar: baz 3 | -------------------------------------------------------------------------------- /tests/fixtures/foo.php: -------------------------------------------------------------------------------- 1 | Hello World

-------------------------------------------------------------------------------- /tests/fixtures/template.twig: -------------------------------------------------------------------------------- 1 |

{{ foo }}

-------------------------------------------------------------------------------- /tests/fixtures/test.csv: -------------------------------------------------------------------------------- 1 | 1,foo 2 | 2,bar 3 | 3,baz 4 | -------------------------------------------------------------------------------- /tests/fixtures/to_be_required.php: -------------------------------------------------------------------------------- 1 |