├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── src ├── Event │ ├── ResponseWasMorphed.php │ ├── ResponseIsMorphing.php │ └── RequestWasMatched.php ├── Exception │ ├── DeleteResourceFailedException.php │ ├── StoreResourceFailedException.php │ ├── UpdateResourceFailedException.php │ ├── UnknownVersionException.php │ ├── ValidationHttpException.php │ ├── RateLimitExceededException.php │ ├── InternalHttpException.php │ ├── ResourceException.php │ └── Handler.php ├── Contract │ ├── Debug │ │ ├── ExceptionHandler.php │ │ └── MessageBagErrors.php │ ├── Http │ │ ├── Parser.php │ │ ├── Validator.php │ │ ├── Request.php │ │ └── RateLimit │ │ │ ├── HasRateLimiter.php │ │ │ └── Throttle.php │ ├── Auth │ │ └── Provider.php │ ├── Transformer │ │ └── Adapter.php │ └── Routing │ │ └── Adapter.php ├── helpers.php ├── Facade │ ├── Route.php │ └── API.php ├── Http │ ├── RateLimit │ │ ├── Throttle │ │ │ ├── Route.php │ │ │ ├── Authenticated.php │ │ │ ├── Unauthenticated.php │ │ │ └── Throttle.php │ │ └── Handler.php │ ├── InternalRequest.php │ ├── Middleware │ │ ├── PrepareController.php │ │ ├── Auth.php │ │ ├── RateLimit.php │ │ └── Request.php │ ├── Validation │ │ ├── Prefix.php │ │ ├── Accept.php │ │ └── Domain.php │ ├── Response │ │ ├── Format │ │ │ ├── Jsonp.php │ │ │ ├── Format.php │ │ │ ├── Json.php │ │ │ └── JsonOptionalFormatting.php │ │ └── Factory.php │ ├── Parser │ │ └── Accept.php │ ├── RequestValidator.php │ ├── Request.php │ └── FormRequest.php ├── Auth │ ├── Provider │ │ ├── Authorization.php │ │ ├── Basic.php │ │ └── JWT.php │ └── Auth.php ├── Routing │ ├── UrlGenerator.php │ ├── ResourceRegistrar.php │ ├── RouteCollection.php │ ├── Helpers.php │ └── Adapter │ │ └── Laravel.php ├── Provider │ ├── ServiceProvider.php │ ├── RoutingServiceProvider.php │ ├── HttpServiceProvider.php │ ├── LumenServiceProvider.php │ ├── LaravelServiceProvider.php │ └── DingoServiceProvider.php ├── Console │ └── Command │ │ ├── Cache.php │ │ ├── Docs.php │ │ └── Routes.php └── Transformer │ ├── Binding.php │ ├── Factory.php │ └── Adapter │ └── Fractal.php ├── LICENSE ├── composer.json ├── readme.md └── config └── api.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [specialtactics] 2 | -------------------------------------------------------------------------------- /src/Event/ResponseWasMorphed.php: -------------------------------------------------------------------------------- 1 | version($version); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Facade/Route.php: -------------------------------------------------------------------------------- 1 | check(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/RateLimit/Throttle/Unauthenticated.php: -------------------------------------------------------------------------------- 1 | check(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Contract/Transformer/Adapter.php: -------------------------------------------------------------------------------- 1 | input() 13 | if ($this->isJson() && isset($this->request)) { 14 | $this->setJson($this->request); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exception/UnknownVersionException.php: -------------------------------------------------------------------------------- 1 | response = $response; 34 | $this->content = &$content; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Event/RequestWasMatched.php: -------------------------------------------------------------------------------- 1 | request = $request; 34 | $this->app = $app; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ----------------- | --- 3 | | Bug? | no|yes 4 | | New Feature? | no|yes 5 | | Framework | Laravel|Lumen 6 | | Framework version | 8.x.y 7 | | Package version | 3.x.y 8 | | PHP version | 7.x.y|8.x.y 9 | 10 | #### Actual Behaviour 11 | 12 | Describe the behaviour you're experiencing. Do not just copy and paste a random error message and expect help. 13 | 14 | 15 | #### Expected Behaviour 16 | 17 | Describe the behaviour you're expecting. 18 | 19 | 20 | #### Steps to Reproduce 21 | 22 | List all the steps needed to reproduce the issue you're having. Make sure to include code (affected models, configurations), 23 | any screenshots and/or other resources that may help us understand what's going on. 24 | 25 | 26 | #### Possible Solutions 27 | 28 | If you have any ideas on how to solve the issue, add them here, otherwise you can omit this part. 29 | -------------------------------------------------------------------------------- /src/Exception/RateLimitExceededException.php: -------------------------------------------------------------------------------- 1 | 60, 'expires' => 60]; 15 | 16 | /** 17 | * Create a new throttle instance. 18 | * 19 | * @param array $options 20 | * @return void 21 | */ 22 | public function __construct(array $options = []) 23 | { 24 | $this->options = array_merge($this->options, $options); 25 | } 26 | 27 | /** 28 | * Get the throttles request limit. 29 | * 30 | * @return int 31 | */ 32 | public function getLimit() 33 | { 34 | return $this->options['limit']; 35 | } 36 | 37 | /** 38 | * Get the time in minutes that the throttles request limit will expire. 39 | * 40 | * @return int 41 | */ 42 | public function getExpires() 43 | { 44 | return $this->options['expires']; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Auth/Provider/Authorization.php: -------------------------------------------------------------------------------- 1 | headers->get('authorization')), $this->getAuthorizationMethod())) { 29 | return true; 30 | } 31 | 32 | throw new BadRequestHttpException; 33 | } 34 | 35 | /** 36 | * Get the providers authorization method. 37 | * 38 | * @return string 39 | */ 40 | abstract public function getAuthorizationMethod(); 41 | } 42 | -------------------------------------------------------------------------------- /src/Routing/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | setRequest($request); 26 | } 27 | 28 | /** 29 | * Set the routes to use from the version. 30 | * 31 | * @param string $version 32 | * @return \Dingo\Api\Routing\UrlGenerator 33 | */ 34 | public function version($version) 35 | { 36 | $this->routes = $this->collections[$version]; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Set the route collection instance. 43 | * 44 | * @param mixed $collections 45 | */ 46 | public function setRouteCollections(mixed $collections) 47 | { 48 | $this->collections = $collections; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Http/Middleware/PrepareController.php: -------------------------------------------------------------------------------- 1 | router = $router; 26 | } 27 | 28 | /** 29 | * Handle the request. 30 | * 31 | * @param \Dingo\Api\Http\Request $request 32 | * @param \Closure $next 33 | * @return mixed 34 | */ 35 | public function handle($request, Closure $next) 36 | { 37 | // To prepare the controller all we need to do is call the current method on the router to fetch 38 | // the current route. This will create a new Dingo\Api\Routing\Route instance and prepare the 39 | // controller by binding it as a singleton in the container. This will result in the 40 | // controller only be instantiated once per request. 41 | $this->router->current(); 42 | 43 | return $next($request); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/Validation/Prefix.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix ?? ''; 26 | } 27 | 28 | /** 29 | * Validate the request has a prefix and if it matches the configured 30 | * API prefix. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @return bool 34 | */ 35 | public function validate(Request $request) 36 | { 37 | $prefix = $this->filterAndExplode($this->prefix); 38 | 39 | $path = $this->filterAndExplode($request->getPathInfo()); 40 | 41 | return ! empty($this->prefix) && $prefix == array_slice($path, 0, count($prefix)); 42 | } 43 | 44 | /** 45 | * Explode array on slash and remove empty values. 46 | * 47 | * @param array $array 48 | * @return array 49 | */ 50 | protected function filterAndExplode($array) 51 | { 52 | return array_filter(explode('/', $array)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Http/Middleware/Auth.php: -------------------------------------------------------------------------------- 1 | router = $router; 35 | $this->auth = $auth; 36 | } 37 | 38 | /** 39 | * Perform authentication before a request is executed. 40 | * 41 | * @param \Illuminate\Http\Request $request 42 | * @param \Closure $next 43 | * @return mixed 44 | */ 45 | public function handle($request, Closure $next) 46 | { 47 | $route = $this->router->getCurrentRoute(); 48 | 49 | if (! $this->auth->check(false)) { 50 | $this->auth->authenticate($route->getAuthenticationProviders()); 51 | } 52 | 53 | return $next($request); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exception/InternalHttpException.php: -------------------------------------------------------------------------------- 1 | response = $response; 31 | 32 | parent::__construct($response->getStatusCode(), $message ?? '', $previous, $headers, $code); 33 | } 34 | 35 | /** 36 | * Get the response of the internal request. 37 | * 38 | * @return \Illuminate\Http\Response 39 | */ 40 | public function getResponse() 41 | { 42 | return $this->response; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - v* 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-24.04 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | stability: [prefer-stable] 18 | versions: [ { php: 8.2, laravel: 10 }, { php: 8.3, laravel: 10 }, { php: 8.4, laravel: 10 }, { php: 8.2, laravel: 11 }, { php: 8.3, laravel: 11 }, { php: 8.4, laravel: 11 }, { php: 8.2, laravel: 12 }, { php: 8.3, laravel: 12 }, { php: 8.4, laravel: 12 } ] 19 | 20 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | # Docs: https://github.com/shivammathur/setup-php 27 | - name: Setup PHP 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.versions.php }} 31 | extensions: dom, curl, libxml, mbstring, zip 32 | ini-values: error_reporting=E_ALL 33 | tools: composer:v2 34 | # todo: Add 35 | coverage: none 36 | 37 | - name: Install dependencies 38 | run: | 39 | composer require "illuminate/contracts=^${{ matrix.versions.laravel }}" --no-update 40 | composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader 41 | 42 | - name: Run phpunit tests 43 | run: composer test 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015, Jason Lewis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Dingo API nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/Exception/ResourceException.php: -------------------------------------------------------------------------------- 1 | errors = new MessageBag; 33 | } else { 34 | $this->errors = is_array($errors) ? new MessageBag($errors) : $errors; 35 | } 36 | 37 | parent::__construct(422, $message ?? '', $previous, $headers, $code); 38 | } 39 | 40 | /** 41 | * Get the errors message bag. 42 | * 43 | * @return \Illuminate\Support\MessageBag 44 | */ 45 | public function getErrors() 46 | { 47 | return $this->errors; 48 | } 49 | 50 | /** 51 | * Determine if message bag has any errors. 52 | * 53 | * @return bool 54 | */ 55 | public function hasErrors() 56 | { 57 | return ! $this->errors->isEmpty(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Auth/Provider/Basic.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 36 | $this->identifier = $identifier; 37 | } 38 | 39 | /** 40 | * Authenticate request with Basic. 41 | * 42 | * @param \Illuminate\Http\Request $request 43 | * @param \Dingo\Api\Routing\Route $route 44 | * @return mixed 45 | */ 46 | public function authenticate(Request $request, Route $route) 47 | { 48 | $this->validateAuthorizationHeader($request); 49 | 50 | if (($response = $this->auth->onceBasic($this->identifier)) && $response->getStatusCode() === 401) { 51 | throw new UnauthorizedHttpException('Basic', 'Invalid authentication credentials.'); 52 | } 53 | 54 | return $this->auth->user(); 55 | } 56 | 57 | /** 58 | * Get the providers authorization method. 59 | * 60 | * @return string 61 | */ 62 | public function getAuthorizationMethod() 63 | { 64 | return 'basic'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Http/Response/Format/Jsonp.php: -------------------------------------------------------------------------------- 1 | callbackName = $callbackName; 23 | } 24 | 25 | /** 26 | * Determine if a callback is valid. 27 | * 28 | * @return bool 29 | */ 30 | protected function hasValidCallback() 31 | { 32 | return $this->request->query->has($this->callbackName); 33 | } 34 | 35 | /** 36 | * Get the callback from the query string. 37 | * 38 | * @return string 39 | */ 40 | protected function getCallback() 41 | { 42 | return $this->request->query->get($this->callbackName); 43 | } 44 | 45 | /** 46 | * Get the response content type. 47 | * 48 | * @return string 49 | */ 50 | public function getContentType() 51 | { 52 | if ($this->hasValidCallback()) { 53 | return 'application/javascript'; 54 | } 55 | 56 | return parent::getContentType(); 57 | } 58 | 59 | /** 60 | * Encode the content to its JSONP representation. 61 | * 62 | * @param mixed $content 63 | * @return string 64 | */ 65 | protected function encode($content) 66 | { 67 | $jsonString = parent::encode($content); 68 | 69 | if ($this->hasValidCallback()) { 70 | return sprintf('%s(%s);', $this->getCallback(), $jsonString); 71 | } 72 | 73 | return $jsonString; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Http/Validation/Accept.php: -------------------------------------------------------------------------------- 1 | accept = $accept; 36 | $this->strict = $strict; 37 | } 38 | 39 | /** 40 | * Validate the accept header on the request. If this fails it will throw 41 | * an HTTP exception that will be caught by the middleware. This 42 | * validator should always be run last and must not return 43 | * a success boolean. 44 | * 45 | * @param \Illuminate\Http\Request $request 46 | * @return bool 47 | * 48 | * @throws \Exception|\Symfony\Component\HttpKernel\Exception\BadRequestHttpException 49 | */ 50 | public function validate(Request $request) 51 | { 52 | try { 53 | $this->accept->parse($request, $this->strict); 54 | } catch (BadRequestHttpException $exception) { 55 | if ($request->getMethod() === 'OPTIONS') { 56 | return true; 57 | } 58 | 59 | throw $exception; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Provider/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['config']->get('api.'.$item); 28 | 29 | if (is_array($value)) { 30 | return $instantiate ? $this->instantiateConfigValues($item, $value) : $value; 31 | } 32 | 33 | return $instantiate ? $this->instantiateConfigValue($item, $value) : $value; 34 | } 35 | 36 | /** 37 | * Instantiate an array of instantiable configuration values. 38 | * 39 | * @param string $item 40 | * @param array $values 41 | * @return array 42 | */ 43 | protected function instantiateConfigValues($item, array $values) 44 | { 45 | foreach ($values as $key => $value) { 46 | $values[$key] = $this->instantiateConfigValue($item, $value); 47 | } 48 | 49 | return $values; 50 | } 51 | 52 | /** 53 | * Instantiate an instantiable configuration value. 54 | * 55 | * @param string $item 56 | * @param mixed $value 57 | * @return mixed 58 | */ 59 | protected function instantiateConfigValue($item, $value) 60 | { 61 | if (is_string($value) && in_array($item, $this->instantiable)) { 62 | return $this->app->make($value); 63 | } 64 | 65 | return $value; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Contract/Routing/Adapter.php: -------------------------------------------------------------------------------- 1 | domain = $domain; 29 | } 30 | 31 | /** 32 | * Validate that the request domain matches the configured domain. 33 | * 34 | * @param \Illuminate\Http\Request $request 35 | * @return bool 36 | */ 37 | public function validate(Request $request) 38 | { 39 | return ! is_null($this->domain) && $request->getHost() === $this->getStrippedDomain(); 40 | } 41 | 42 | /** 43 | * Strip the protocol from a domain. 44 | * 45 | * @param string $domain 46 | * @return string 47 | */ 48 | protected function stripProtocol($domain) 49 | { 50 | if (Str::contains($domain, '://')) { 51 | $domain = substr($domain, strpos($domain, '://') + 3); 52 | } 53 | 54 | return $domain; 55 | } 56 | 57 | /** 58 | * Strip the port from a domain. 59 | * 60 | * @param $domain 61 | * @return mixed 62 | */ 63 | protected function stripPort($domain) 64 | { 65 | if ($domainStripped = preg_replace(self::PATTERN_STRIP_PROTOCOL, '', $domain)) { 66 | return $domainStripped; 67 | } 68 | 69 | return $domain; 70 | } 71 | 72 | /** 73 | * Get the domain stripped from protocol and port. 74 | * 75 | * @return mixed 76 | */ 77 | protected function getStrippedDomain() 78 | { 79 | return $this->stripPort($this->stripProtocol($this->domain)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Provider/RoutingServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRouter(); 19 | 20 | $this->registerUrlGenerator(); 21 | } 22 | 23 | /** 24 | * Register the router. 25 | */ 26 | protected function registerRouter() 27 | { 28 | $this->app->singleton('api.router', function ($app) { 29 | $router = new Router( 30 | $app[Adapter::class], 31 | $app[ExceptionHandler::class], 32 | $app, 33 | $this->config('domain'), 34 | $this->config('prefix') 35 | ); 36 | 37 | $router->setConditionalRequest($this->config('conditionalRequest')); 38 | 39 | return $router; 40 | }); 41 | 42 | $this->app->singleton(ResourceRegistrar::class, function ($app) { 43 | return new ResourceRegistrar($app[Router::class]); 44 | }); 45 | } 46 | 47 | /** 48 | * Register the URL generator. 49 | */ 50 | protected function registerUrlGenerator() 51 | { 52 | $this->app->singleton('api.url', function ($app) { 53 | $url = new UrlGenerator($app['request']); 54 | 55 | $url->setRouteCollections($app[Router::class]->getRoutes()); 56 | 57 | $url->setKeyResolver(function () { 58 | return $this->app->make('config')->get('app.key'); 59 | }); 60 | 61 | return $url; 62 | }); 63 | } 64 | 65 | /** 66 | * Get the URL generator request rebinder. 67 | * 68 | * @return \Closure 69 | */ 70 | private function requestRebinder() 71 | { 72 | return function ($app, $request) { 73 | $app['api.url']->setRequest($request); 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Routing/ResourceRegistrar.php: -------------------------------------------------------------------------------- 1 | router = $router; 26 | } 27 | 28 | /** 29 | * Route a resource to a controller. 30 | * 31 | * @param string $name 32 | * @param string $controller 33 | * @param array $options 34 | * @return void 35 | */ 36 | public function register($name, $controller, array $options = []) 37 | { 38 | if (isset($options['parameters']) && ! isset($this->parameters)) { 39 | $this->parameters = $options['parameters']; 40 | } 41 | 42 | // If the resource name contains a slash, we will assume the developer wishes to 43 | // register these resource routes with a prefix so we will set that up out of 44 | // the box so they don't have to mess with it. Otherwise, we will continue. 45 | if (Str::contains($name, '/')) { 46 | $this->prefixedResource($name, $controller, $options); 47 | 48 | return; 49 | } 50 | 51 | // We need to extract the base resource from the resource name. Nested resources 52 | // are supported in the framework, but we need to know what name to use for a 53 | // place-holder on the route parameters, which should be the base resources. 54 | $base = $this->getResourceWildcard(last(explode('.', $name))); 55 | 56 | $defaults = $this->resourceDefaults; 57 | 58 | foreach ($this->getResourceMethods($defaults, $options) as $m) { 59 | $this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Http/Response/Format/Format.php: -------------------------------------------------------------------------------- 1 | request = $request; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Set the response instance. 43 | * 44 | * @param \Illuminate\Http\Response $response 45 | * @return \Dingo\Api\Http\Response\Format\Format 46 | */ 47 | public function setResponse($response) 48 | { 49 | $this->response = $response; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Set the formats' options. 56 | * 57 | * @param array $options 58 | * @return \Dingo\Api\Http\Response\Format\Format 59 | */ 60 | public function setOptions(array $options) 61 | { 62 | $this->options = $options; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Format an Eloquent model. 69 | * 70 | * @param \Illuminate\Database\Eloquent\Model $model 71 | * @return string 72 | */ 73 | abstract public function formatEloquentModel($model); 74 | 75 | /** 76 | * Format an Eloquent collection. 77 | * 78 | * @param \Illuminate\Database\Eloquent\Collection $collection 79 | * @return string 80 | */ 81 | abstract public function formatEloquentCollection($collection); 82 | 83 | /** 84 | * Format an array or instance implementing Arrayable. 85 | * 86 | * @param array|\Illuminate\Contracts\Support\Arrayable $content 87 | * @return string 88 | */ 89 | abstract public function formatArray($content); 90 | 91 | /** 92 | * Get the response content type. 93 | * 94 | * @return string 95 | */ 96 | abstract public function getContentType(); 97 | } 98 | -------------------------------------------------------------------------------- /src/Http/Parser/Accept.php: -------------------------------------------------------------------------------- 1 | standardsTree = $standardsTree; 51 | $this->subtype = $subtype; 52 | $this->version = $version; 53 | $this->format = $format; 54 | } 55 | 56 | /** 57 | * Parse the accept header on the incoming request. If strict is enabled 58 | * then the accept header must be available and must be a valid match. 59 | * 60 | * @param \Illuminate\Http\Request $request 61 | * @param bool $strict 62 | * @return array 63 | * 64 | * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException 65 | */ 66 | public function parse(Request $request, $strict = false) 67 | { 68 | $pattern = '/application\/'.$this->standardsTree.'\.('.$this->subtype.')\.([\w\d\.\-]+)\+([\w]+)/'; 69 | 70 | if (! preg_match($pattern, $request->header('accept', ''), $matches)) { 71 | if ($strict) { 72 | throw new BadRequestHttpException('Accept header could not be properly parsed because of a strict matching process.'); 73 | } 74 | 75 | $default = 'application/'.$this->standardsTree.'.'.$this->subtype.'.'.$this->version.'+'.$this->format; 76 | 77 | preg_match($pattern, $default, $matches); 78 | } 79 | 80 | return array_combine(['subtype', 'version', 'format'], array_slice($matches, 1)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Auth/Provider/JWT.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 30 | } 31 | 32 | /** 33 | * Authenticate request with a JWT. 34 | * 35 | * @param \Illuminate\Http\Request $request 36 | * @param \Dingo\Api\Routing\Route $route 37 | * @return mixed 38 | */ 39 | public function authenticate(Request $request, Route $route) 40 | { 41 | $token = $this->getToken($request); 42 | 43 | try { 44 | if (! $user = $this->auth->setToken($token)->authenticate()) { 45 | throw new UnauthorizedHttpException('JWTAuth', 'Unable to authenticate with invalid token.'); 46 | } 47 | } catch (JWTException $exception) { 48 | throw new UnauthorizedHttpException('JWTAuth', $exception->getMessage(), $exception); 49 | } 50 | 51 | return $user; 52 | } 53 | 54 | /** 55 | * Get the JWT from the request. 56 | * 57 | * @param \Illuminate\Http\Request $request 58 | * @return string 59 | * 60 | * @throws \Exception 61 | */ 62 | protected function getToken(Request $request) 63 | { 64 | try { 65 | $this->validateAuthorizationHeader($request); 66 | 67 | $token = $this->parseAuthorizationHeader($request); 68 | } catch (Exception $exception) { 69 | if (! $token = $request->query('token', false)) { 70 | throw $exception; 71 | } 72 | } 73 | 74 | return $token; 75 | } 76 | 77 | /** 78 | * Parse JWT from the authorization header. 79 | * 80 | * @param \Illuminate\Http\Request $request 81 | * @return string 82 | */ 83 | protected function parseAuthorizationHeader(Request $request) 84 | { 85 | return trim(str_ireplace($this->getAuthorizationMethod(), '', $request->header('authorization'))); 86 | } 87 | 88 | /** 89 | * Get the providers authorization method. 90 | * 91 | * @return string 92 | */ 93 | public function getAuthorizationMethod() 94 | { 95 | return 'bearer'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Routing/RouteCollection.php: -------------------------------------------------------------------------------- 1 | routes[] = $route; 41 | 42 | $this->addLookups($route); 43 | 44 | return $route; 45 | } 46 | 47 | /** 48 | * Add route lookups. 49 | * 50 | * @param \Dingo\Api\Routing\Route $route 51 | * @return void 52 | */ 53 | protected function addLookups(Route $route) 54 | { 55 | $action = $route->getAction(); 56 | 57 | if (isset($action['as'])) { 58 | $this->names[$action['as']] = $route; 59 | } 60 | 61 | if (isset($action['controller'])) { 62 | $this->actions[$action['controller']] = $route; 63 | } 64 | } 65 | 66 | /** 67 | * Get a route by name. 68 | * 69 | * @param string $name 70 | * @return \Dingo\Api\Routing\Route|null 71 | */ 72 | public function getByName($name) 73 | { 74 | return isset($this->names[$name]) ? $this->names[$name] : null; 75 | } 76 | 77 | /** 78 | * Get a route by action. 79 | * 80 | * @param string $action 81 | * @return \Dingo\Api\Routing\Route|null 82 | */ 83 | public function getByAction($action) 84 | { 85 | return isset($this->actions[$action]) ? $this->actions[$action] : null; 86 | } 87 | 88 | /** 89 | * Get all routes. 90 | * 91 | * @return array 92 | */ 93 | public function getRoutes() 94 | { 95 | return $this->routes; 96 | } 97 | 98 | /** 99 | * Get an iterator for the items. 100 | * 101 | * @return \ArrayIterator 102 | */ 103 | public function getIterator(): \Traversable 104 | { 105 | return new ArrayIterator($this->getRoutes()); 106 | } 107 | 108 | /** 109 | * Count the number of items in the collection. 110 | * 111 | * @return int 112 | */ 113 | public function count(): int 114 | { 115 | return count($this->getRoutes()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-ecosystem-for-laravel/dingo-api", 3 | "description": "A RESTful API package for the Laravel and Lumen frameworks.", 4 | "keywords": [ 5 | "api", 6 | "dingo", 7 | "laravel", 8 | "restful" 9 | ], 10 | "license": "BSD-3-Clause", 11 | "authors": [ 12 | { 13 | "name": "Jason Lewis", 14 | "email": "jason.lewis1991@gmail.com" 15 | }, 16 | { 17 | "name": "Max Snow", 18 | "email": "contact@maxsnow.me" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.2", 23 | "illuminate/routing": "^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/support": "^9.0|^10.0|^11.0|^12.0", 25 | "php-open-source-saver/fractal": "^1.0" 26 | }, 27 | "require-dev": { 28 | "friendsofphp/php-cs-fixer": "~3", 29 | "illuminate/auth": "^9.0|^10.0|^11.0|^12.0", 30 | "illuminate/cache": "^9.0|^10.0|^11.0|^12.0", 31 | "illuminate/console": "^9.0|^10.0|^11.0|^12.0", 32 | "illuminate/database": "^9.0|^10.0|^11.0|^12.0", 33 | "illuminate/events": "^9.0|^10.0|^11.0|^12.0", 34 | "illuminate/filesystem": "^9.0|^10.0|^11.0|^12.0", 35 | "illuminate/log": "^9.0|^10.0|^11.0|^12.0", 36 | "illuminate/pagination": "^9.0|^10.0|^11.0|^12.0", 37 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0", 38 | "illuminate/translation": "^9.0|^10.0|^11.0|^12.0", 39 | "mockery/mockery": "~1.0", 40 | "php-open-source-saver/jwt-auth": "^1.4 | ^2.8", 41 | "phpunit/phpunit": "^9.5|^10.5", 42 | "squizlabs/php_codesniffer": "~2.0" 43 | }, 44 | "suggest": { 45 | "php-open-source-saver/jwt-auth": "Protect your API with JSON Web Tokens.", 46 | "specialtactics/laravel-api-boilerplate": "API Boilerplate for Laravel", 47 | "dingo/blueprint": "Legacy package which can produce API docs from dingo/api" 48 | }, 49 | "autoload": { 50 | "psr-4": { 51 | "Dingo\\Api\\": "src/" 52 | }, 53 | "files": [ 54 | "src/helpers.php" 55 | ] 56 | }, 57 | "autoload-dev": { 58 | "psr-4": { 59 | "Dingo\\Api\\Tests\\": "tests/" 60 | } 61 | }, 62 | "extra": { 63 | "laravel": { 64 | "providers": [ 65 | "Dingo\\Api\\Provider\\LaravelServiceProvider" 66 | ], 67 | "aliases": { 68 | "API": "Dingo\\Api\\Facade\\API" 69 | } 70 | } 71 | }, 72 | "config": { 73 | "sort-packages": true 74 | }, 75 | "minimum-stability": "dev", 76 | "prefer-stable": true, 77 | "scripts": { 78 | "test": [ 79 | "vendor/bin/phpunit" 80 | ], 81 | "lint": [ 82 | "vendor/bin/phpcs" 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Http/RequestValidator.php: -------------------------------------------------------------------------------- 1 | container = $container; 40 | } 41 | 42 | /** 43 | * Replace the validators. 44 | * 45 | * @param array $validators 46 | * @return void 47 | */ 48 | public function replace(array $validators) 49 | { 50 | $this->validators = $validators; 51 | } 52 | 53 | /** 54 | * Merge an array of validators. 55 | * 56 | * @param array $validators 57 | * @return void 58 | */ 59 | public function merge(array $validators) 60 | { 61 | $this->validators = array_merge($this->validators, $validators); 62 | } 63 | 64 | /** 65 | * Extend the validators. 66 | * 67 | * @param string|\Dingo\Api\Http\Validator $validator 68 | * @return void 69 | */ 70 | public function extend($validator) 71 | { 72 | $this->validators[] = $validator; 73 | } 74 | 75 | /** 76 | * Validate a request. 77 | * 78 | * @param \Illuminate\Http\Request $request 79 | * @return bool 80 | */ 81 | public function validateRequest(IlluminateRequest $request) 82 | { 83 | $passed = false; 84 | 85 | foreach ($this->validators as $validator) { 86 | $validator = $this->container->make($validator); 87 | 88 | if ($validator instanceof Validator && $validator->validate($request)) { 89 | $passed = true; 90 | } 91 | } 92 | 93 | // The accept validator will always be run once any of the previous validators have 94 | // been run. This ensures that we only run the accept validator once we know we 95 | // have a request that is targeting the API. 96 | if ($passed) { 97 | $this->container->make(Accept::class)->validate($request); 98 | } 99 | 100 | return $passed; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Facade/API.php: -------------------------------------------------------------------------------- 1 | register($callback); 29 | } 30 | 31 | /** 32 | * Register a class transformer. 33 | * 34 | * @param string $class 35 | * @param string|\Closure $transformer 36 | * @return \Dingo\Api\Transformer\Binding 37 | */ 38 | public static function transform($class, $transformer) 39 | { 40 | return static::$app['api.transformer']->register($class, $transformer); 41 | } 42 | 43 | /** 44 | * Get the authenticator. 45 | * 46 | * @return \Dingo\Api\Auth\Auth 47 | */ 48 | public static function auth() 49 | { 50 | return static::$app['api.auth']; 51 | } 52 | 53 | /** 54 | * Get the authenticated user. 55 | * 56 | * @return \Illuminate\Auth\GenericUser|\Illuminate\Database\Eloquent\Model 57 | */ 58 | public static function user() 59 | { 60 | return static::$app['api.auth']->user(); 61 | } 62 | 63 | /** 64 | * Determine if a request is internal. 65 | * 66 | * @return bool 67 | */ 68 | public static function internal() 69 | { 70 | return static::$app['api.router']->getCurrentRequest() instanceof InternalRequest; 71 | } 72 | 73 | /** 74 | * Get the response factory to begin building a response. 75 | * 76 | * @return \Dingo\Api\Http\Response\Factory 77 | */ 78 | public static function response() 79 | { 80 | return static::$app['api.http.response']; 81 | } 82 | 83 | /** 84 | * Get the API router instance. 85 | * 86 | * @return \Dingo\Api\Routing\Router 87 | */ 88 | public static function router() 89 | { 90 | return static::$app['api.router']; 91 | } 92 | 93 | /** 94 | * Get the API route of the given name, and optionally specify the API version. 95 | * 96 | * @param string $routeName 97 | * @param array $parameters 98 | * @param string $apiVersion 99 | * @return string 100 | */ 101 | public static function route($routeName, $parameters = [], $apiVersion = 'v1') 102 | { 103 | return static::$app['api.url']->version($apiVersion)->route($routeName, $parameters); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Http/Middleware/RateLimit.php: -------------------------------------------------------------------------------- 1 | router = $router; 38 | $this->handler = $handler; 39 | } 40 | 41 | /** 42 | * Perform rate limiting before a request is executed. 43 | * 44 | * @param \Dingo\Api\Http\Request $request 45 | * @param \Closure $next 46 | * @return mixed 47 | * 48 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 49 | */ 50 | public function handle($request, Closure $next) 51 | { 52 | if ($request instanceof InternalRequest) { 53 | return $next($request); 54 | } 55 | 56 | $route = $this->router->getCurrentRoute(); 57 | 58 | if ($route->hasThrottle()) { 59 | $this->handler->setThrottle($route->getThrottle()); 60 | } 61 | 62 | $this->handler->rateLimitRequest($request, $route->getRateLimit(), $route->getRateLimitExpiration()); 63 | 64 | if ($this->handler->exceededRateLimit()) { 65 | throw new RateLimitExceededException('You have exceeded your rate limit.', null, $this->getHeaders()); 66 | } 67 | 68 | $response = $next($request); 69 | 70 | if ($this->handler->requestWasRateLimited()) { 71 | return $this->responseWithHeaders($response); 72 | } 73 | 74 | return $response; 75 | } 76 | 77 | /** 78 | * Send the response with the rate limit headers. 79 | * 80 | * @param \Dingo\Api\Http\Response $response 81 | * @return \Dingo\Api\Http\Response 82 | */ 83 | protected function responseWithHeaders($response) 84 | { 85 | foreach ($this->getHeaders() as $key => $value) { 86 | $response->headers->set($key, $value); 87 | } 88 | 89 | return $response; 90 | } 91 | 92 | /** 93 | * Get the headers for the response. 94 | * 95 | * @return array 96 | */ 97 | protected function getHeaders() 98 | { 99 | return [ 100 | 'X-RateLimit-Limit' => $this->handler->getThrottleLimit(), 101 | 'X-RateLimit-Remaining' => $this->handler->getRemainingLimit(), 102 | 'X-RateLimit-Reset' => $this->handler->getRateLimitReset(), 103 | ]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Console/Command/Cache.php: -------------------------------------------------------------------------------- 1 | files = $files; 59 | $this->router = $router; 60 | $this->adapter = $adapter; 61 | 62 | parent::__construct(); 63 | } 64 | 65 | /** 66 | * Execute the console command. 67 | * 68 | * @return mixed 69 | */ 70 | public function handle() 71 | { 72 | $this->callSilent('route:clear'); 73 | 74 | $app = $this->getFreshApplication(); 75 | 76 | $this->call('route:cache'); 77 | 78 | $routes = $app['api.router']->getAdapterRoutes(); 79 | 80 | foreach ($routes as $collection) { 81 | foreach ($collection as $route) { 82 | $app['api.router.adapter']->prepareRouteForSerialization($route); 83 | } 84 | } 85 | 86 | $stub = "app('api.router')->setAdapterRoutes(unserialize(base64_decode('{{routes}}')));"; 87 | $path = $this->laravel->getCachedRoutesPath(); 88 | 89 | if (! $this->files->exists($path)) { 90 | $stub = "files->append( 94 | $path, 95 | str_replace('{{routes}}', base64_encode(serialize($routes)), $stub) 96 | ); 97 | } 98 | 99 | /** 100 | * Get a fresh application instance. 101 | * 102 | * @return \Illuminate\Contracts\Container\Container 103 | */ 104 | protected function getFreshApplication() 105 | { 106 | if (method_exists($this->laravel, 'bootstrapPath')) { 107 | $app = require $this->laravel->bootstrapPath().'/app.php'; 108 | } else { 109 | $app = require $this->laravel->basePath().'/bootstrap/app.php'; 110 | } 111 | 112 | $app->make(Kernel::class)->bootstrap(); 113 | 114 | return $app; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Http/Request.php: -------------------------------------------------------------------------------- 1 | query->all(), $old->request->all(), $old->attributes->all(), 37 | $old->cookies->all(), $old->files->all(), $old->server->all(), $old->content 38 | ); 39 | 40 | try { 41 | $session = $old->getSession(); 42 | if ($session instanceof SymfonySessionDecorator) { 43 | $new->setLaravelSession($session->store); 44 | } 45 | } catch (SessionNotFoundException $exception) { 46 | } 47 | 48 | $new->setRouteResolver($old->getRouteResolver()); 49 | $new->setUserResolver($old->getUserResolver()); 50 | 51 | return $new; 52 | } 53 | 54 | /** 55 | * Get the defined version. 56 | * 57 | * @return string 58 | */ 59 | public function version() 60 | { 61 | $this->parseAcceptHeader(); 62 | 63 | return $this->accept['version']; 64 | } 65 | 66 | /** 67 | * Get the defined subtype. 68 | * 69 | * @return string 70 | */ 71 | public function subtype() 72 | { 73 | $this->parseAcceptHeader(); 74 | 75 | return $this->accept['subtype']; 76 | } 77 | 78 | /** 79 | * Get the expected format type. 80 | * 81 | * @return string 82 | */ 83 | public function format($default = 'html') 84 | { 85 | $this->parseAcceptHeader(); 86 | 87 | return $this->accept['format'] ?: parent::format($default); 88 | } 89 | 90 | /** 91 | * Parse the accept header. 92 | * 93 | * @return void 94 | */ 95 | protected function parseAcceptHeader() 96 | { 97 | if ($this->accept) { 98 | return; 99 | } 100 | 101 | $this->accept = static::$acceptParser->parse($this); 102 | } 103 | 104 | /** 105 | * Set the accept parser instance. 106 | * 107 | * @param \Dingo\Api\Http\Parser\Accept $acceptParser 108 | * @return void 109 | */ 110 | public static function setAcceptParser(Accept $acceptParser) 111 | { 112 | static::$acceptParser = $acceptParser; 113 | } 114 | 115 | /** 116 | * Get the accept parser instance. 117 | * 118 | * @return \Dingo\Api\Http\Parser\Accept 119 | */ 120 | public static function getAcceptParser() 121 | { 122 | return static::$acceptParser; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Transformer/Binding.php: -------------------------------------------------------------------------------- 1 | container = $container; 58 | $this->resolver = $resolver; 59 | $this->parameters = $parameters; 60 | $this->callback = $callback; 61 | } 62 | 63 | /** 64 | * Resolve a transformer binding instance. 65 | * 66 | * @return object 67 | * 68 | * @throws \RuntimeException 69 | */ 70 | public function resolveTransformer() 71 | { 72 | if (is_string($this->resolver)) { 73 | return $this->container->make($this->resolver); 74 | } elseif (is_callable($this->resolver)) { 75 | return call_user_func($this->resolver, $this->container); 76 | } elseif (is_object($this->resolver)) { 77 | return $this->resolver; 78 | } 79 | 80 | throw new RuntimeException('Unable to resolve transformer binding.'); 81 | } 82 | 83 | /** 84 | * Fire the binding callback. 85 | * 86 | * @param string|array $parameters 87 | * @return void 88 | */ 89 | public function fireCallback($parameters = null) 90 | { 91 | if (is_callable($this->callback)) { 92 | call_user_func_array($this->callback, func_get_args()); 93 | } 94 | } 95 | 96 | /** 97 | * Get the binding parameters. 98 | * 99 | * @return array 100 | */ 101 | public function getParameters() 102 | { 103 | return $this->parameters; 104 | } 105 | 106 | /** 107 | * Set the meta data for the binding. 108 | * 109 | * @param array $meta 110 | * @return void 111 | */ 112 | public function setMeta(array $meta) 113 | { 114 | $this->meta = $meta; 115 | } 116 | 117 | /** 118 | * Add a meta data key/value pair. 119 | * 120 | * @param string $key 121 | * @param mixed $value 122 | * @return void 123 | */ 124 | public function addMeta($key, $value) 125 | { 126 | $this->meta[$key] = $value; 127 | } 128 | 129 | /** 130 | * Get the binding meta data. 131 | * 132 | * @return array 133 | */ 134 | public function getMeta() 135 | { 136 | return $this->meta; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Http/Response/Format/Json.php: -------------------------------------------------------------------------------- 1 | getTable()); 26 | 27 | if (! $model::$snakeAttributes) { 28 | $key = Str::camel($key); 29 | } 30 | 31 | return $this->encode([$key => $model->toArray()]); 32 | } 33 | 34 | /** 35 | * Format an Eloquent collection. 36 | * 37 | * @param \Illuminate\Database\Eloquent\Collection $collection 38 | * @return string 39 | */ 40 | public function formatEloquentCollection($collection) 41 | { 42 | if ($collection->isEmpty()) { 43 | return $this->encode([]); 44 | } 45 | 46 | $model = $collection->first(); 47 | $key = Str::plural($model->getTable()); 48 | 49 | if (! $model::$snakeAttributes) { 50 | $key = Str::camel($key); 51 | } 52 | 53 | return $this->encode([$key => $collection->toArray()]); 54 | } 55 | 56 | /** 57 | * Format an array or instance implementing Arrayable. 58 | * 59 | * @param array|\Illuminate\Contracts\Support\Arrayable $content 60 | * @return string 61 | */ 62 | public function formatArray($content) 63 | { 64 | $content = $this->morphToArray($content); 65 | 66 | array_walk_recursive($content, function (&$value) { 67 | $value = $this->morphToArray($value); 68 | }); 69 | 70 | return $this->encode($content); 71 | } 72 | 73 | /** 74 | * Get the response content type. 75 | * 76 | * @return string 77 | */ 78 | public function getContentType() 79 | { 80 | return 'application/json'; 81 | } 82 | 83 | /** 84 | * Morph a value to an array. 85 | * 86 | * @param array|\Illuminate\Contracts\Support\Arrayable $value 87 | * @return array 88 | */ 89 | protected function morphToArray($value) 90 | { 91 | return $value instanceof Arrayable ? $value->toArray() : $value; 92 | } 93 | 94 | /** 95 | * Encode the content to its JSON representation. 96 | * 97 | * @param mixed $content 98 | * @return string 99 | */ 100 | protected function encode($content) 101 | { 102 | $jsonEncodeOptions = []; 103 | 104 | // Here is a place, where any available JSON encoding options, that 105 | // deal with users' requirements to JSON response formatting and 106 | // structure, can be conveniently applied to tweak the output. 107 | 108 | if ($this->isJsonPrettyPrintEnabled()) { 109 | $jsonEncodeOptions[] = JSON_PRETTY_PRINT; 110 | $jsonEncodeOptions[] = JSON_UNESCAPED_UNICODE; 111 | } 112 | 113 | $encodedString = $this->performJsonEncoding($content, $jsonEncodeOptions); 114 | 115 | if ($this->isCustomIndentStyleRequired()) { 116 | $encodedString = $this->indentPrettyPrintedJson( 117 | $encodedString, 118 | $this->options['indent_style'] 119 | ); 120 | } 121 | 122 | return $encodedString; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Note: This is an official and reasonably maintained fork of the popular https://github.com/dingo/api repository by one of the maintainers of that project. The reason it was forked is due to broken integrations with CI tools such as travis (which can only be fixed by owner), and in general to be able to better support the project and ensure non-breaking updates. 2 | 3 | In order to move to this repo, you merely need to update your composer file. All the namespaces and other aspects of the project are the same. Example instructions are below, to use the latest version: 4 | 5 | ```bash 6 | composer remove dingo/api 7 | composer require api-ecosystem-for-laravel/dingo-api 8 | ``` 9 | 10 | Please note, we do not actively maintain the Lumen support of this project. If you are still using Lumen, we recommend you migrate to Laravel. 11 | 12 | --- 13 | 14 | ![](https://cloud.githubusercontent.com/assets/829059/9216039/82be51cc-40f6-11e5-88f5-f0cbd07bcc39.png) 15 | 16 | The Dingo API package is meant to provide you, the developer, with a set of tools to help you easily and quickly build your own API. While the goal of this package is to remain as flexible as possible it still won't cover all situations and solve all problems. 17 | 18 | [![CI Tests](https://github.com/api-ecosystem-for-laravel/dingo-api/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/api-ecosystem-for-laravel/dingo-api/actions) 19 | [![License](https://img.shields.io/packagist/l/api-ecosystem-for-laravel/dingo-api.svg?style=flat-square)](LICENSE) 20 | [![Development Version](https://img.shields.io/packagist/vpre/api-ecosystem-for-laravel/dingo-api.svg?style=flat-square)](https://packagist.org/packages/api-ecosystem-for-laravel/dingo-api) 21 | [![Monthly Installs](https://img.shields.io/packagist/dm/api-ecosystem-for-laravel/dingo-api.svg?style=flat-square)](https://packagist.org/packages/api-ecosystem-for-laravel/dingo-api) 22 | 23 | ## Features 24 | 25 | This package provides tools for the following, and more: 26 | 27 | - Content Negotiation 28 | - Multiple Authentication Adapters 29 | - API Versioning 30 | - Rate Limiting 31 | - Response Transformers and Formatters 32 | - Error and Exception Handling 33 | - Internal Requests 34 | - API Blueprint Documentation 35 | 36 | ## Documentation 37 | 38 | Please refer to our extensive [Wiki documentation](https://github.com/api-ecosystem-for-laravel/dingo-api/wiki) for more information. 39 | 40 | [![DeepWiki](https://img.shields.io/badge/DeepWiki-api--ecosystem--for--laravel%2Fdingo--api-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/api-ecosystem-for-laravel/dingo-api) 41 | 42 | 43 | ## API Boilerplate 44 | 45 | If you are looking to start a new project from scratch, consider using the [Laravel API Boilerplate](https://github.com/specialtactics/laravel-api-boilerplate), which builds on top of the dingo-api package, and adds a lot of great features for API development. 46 | 47 | ## Support 48 | 49 | For answers you may not find in the Wiki, avoid posting issues. Feel free to ask for support on the dedicated [Slack](https://larachat.slack.com/messages/api/) room. Make sure to mention **specialtactics** so he is notified. 50 | 51 | Alternatively, you can start a [new discussion in the Q&A category](https://github.com/api-ecosystem-for-laravel/dingo-api/discussions/categories/q-a). 52 | 53 | ## License 54 | 55 | This package is licensed under the [BSD 3-Clause license](http://opensource.org/licenses/BSD-3-Clause). 56 | -------------------------------------------------------------------------------- /src/Provider/HttpServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRateLimiting(); 34 | 35 | $this->registerHttpValidation(); 36 | 37 | $this->registerHttpParsers(); 38 | 39 | $this->registerResponseFactory(); 40 | 41 | $this->registerMiddleware(); 42 | } 43 | 44 | /** 45 | * Register the rate limiting. 46 | * 47 | * @return void 48 | */ 49 | protected function registerRateLimiting() 50 | { 51 | $this->app->singleton('api.limiting', function ($app) { 52 | return new RateLimitHandler($app, $app['cache'], $this->config('throttling')); 53 | }); 54 | } 55 | 56 | /** 57 | * Register the HTTP validation. 58 | * 59 | * @return void 60 | */ 61 | protected function registerHttpValidation() 62 | { 63 | $this->app->singleton('api.http.validator', function ($app) { 64 | return new RequestValidator($app); 65 | }); 66 | 67 | $this->app->singleton(Domain::class, function ($app) { 68 | return new Validation\Domain($this->config('domain')); 69 | }); 70 | 71 | $this->app->singleton(Prefix::class, function ($app) { 72 | return new Validation\Prefix($this->config('prefix')); 73 | }); 74 | 75 | $this->app->singleton(Accept::class, function ($app) { 76 | return new Validation\Accept( 77 | $this->app[AcceptParser::class], 78 | $this->config('strict') 79 | ); 80 | }); 81 | } 82 | 83 | /** 84 | * Register the HTTP parsers. 85 | * 86 | * @return void 87 | */ 88 | protected function registerHttpParsers() 89 | { 90 | $this->app->singleton(AcceptParser::class, function ($app) { 91 | return new AcceptParser( 92 | $this->config('standardsTree'), 93 | $this->config('subtype'), 94 | $this->config('version'), 95 | $this->config('defaultFormat') 96 | ); 97 | }); 98 | } 99 | 100 | /** 101 | * Register the response factory. 102 | * 103 | * @return void 104 | */ 105 | protected function registerResponseFactory() 106 | { 107 | $this->app->singleton('api.http.response', function ($app) { 108 | return new ResponseFactory($app[Factory::class]); 109 | }); 110 | } 111 | 112 | /** 113 | * Register the middleware. 114 | * 115 | * @return void 116 | */ 117 | protected function registerMiddleware() 118 | { 119 | $this->app->singleton(Request::class, function ($app) { 120 | $middleware = new Middleware\Request( 121 | $app, 122 | $app[ExceptionHandler::class], 123 | $app[Router::class], 124 | $app[RequestValidator::class], 125 | $app['events'] 126 | ); 127 | 128 | $middleware->setMiddlewares($this->config('middleware', false)); 129 | 130 | return $middleware; 131 | }); 132 | 133 | $this->app->singleton(AuthMiddleware::class, function ($app) { 134 | return new Middleware\Auth($app[Router::class], $app[Auth::class]); 135 | }); 136 | 137 | $this->app->singleton(RateLimit::class, function ($app) { 138 | return new Middleware\RateLimit($app[Router::class], $app[Handler::class]); 139 | }); 140 | 141 | $this->app->singleton(PrepareController::class, function ($app) { 142 | return new Middleware\PrepareController($app[Router::class]); 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Routing/Helpers.php: -------------------------------------------------------------------------------- 1 | throttles[] = compact('class', 'options'); 56 | } 57 | 58 | /** 59 | * Rate limit controller methods. 60 | * 61 | * @param int $limit 62 | * @param int $expires 63 | * @param array $options 64 | * @return void 65 | */ 66 | protected function rateLimit($limit, $expires, array $options = []) 67 | { 68 | $this->rateLimit[] = compact('limit', 'expires', 'options'); 69 | } 70 | 71 | /** 72 | * Add scopes to controller methods. 73 | * 74 | * @param string|array $scopes 75 | * @param array $options 76 | * @return void 77 | */ 78 | protected function scopes($scopes, array $options = []) 79 | { 80 | $scopes = $this->getPropertyValue($scopes); 81 | 82 | $this->scopes[] = compact('scopes', 'options'); 83 | } 84 | 85 | /** 86 | * Authenticate with certain providers on controller methods. 87 | * 88 | * @param string|array $providers 89 | * @param array $options 90 | * @return void 91 | */ 92 | protected function authenticateWith($providers, array $options = []) 93 | { 94 | $providers = $this->getPropertyValue($providers); 95 | 96 | $this->authenticationProviders[] = compact('providers', 'options'); 97 | } 98 | 99 | /** 100 | * Prepare a property value. 101 | * 102 | * @param string|array $value 103 | * @return array 104 | */ 105 | protected function getPropertyValue($value) 106 | { 107 | return is_string($value) ? explode('|', $value) : $value; 108 | } 109 | 110 | /** 111 | * Get the controllers rate limiting throttles. 112 | * 113 | * @return array 114 | */ 115 | public function getThrottles() 116 | { 117 | return $this->throttles; 118 | } 119 | 120 | /** 121 | * Get the controllers rate limit and expiration. 122 | * 123 | * @return array 124 | */ 125 | public function getRateLimit() 126 | { 127 | return $this->rateLimit; 128 | } 129 | 130 | /** 131 | * Get the controllers scopes. 132 | * 133 | * @return array 134 | */ 135 | public function getScopes() 136 | { 137 | return $this->scopes; 138 | } 139 | 140 | /** 141 | * Get the controllers authentication providers. 142 | * 143 | * @return array 144 | */ 145 | public function getAuthenticationProviders() 146 | { 147 | return $this->authenticationProviders; 148 | } 149 | 150 | /** 151 | * Get the internal dispatcher instance. 152 | * 153 | * @return \Dingo\Api\Dispatcher 154 | */ 155 | public function api() 156 | { 157 | return app(Dispatcher::class); 158 | } 159 | 160 | /** 161 | * Get the authenticated user. 162 | * 163 | * @return mixed 164 | */ 165 | protected function user() 166 | { 167 | return app(Auth::class)->user(); 168 | } 169 | 170 | /** 171 | * Get the auth instance. 172 | * 173 | * @return \Dingo\Api\Auth\Auth 174 | */ 175 | protected function auth() 176 | { 177 | return app(Auth::class); 178 | } 179 | 180 | /** 181 | * Get the response factory instance. 182 | * 183 | * @return \Dingo\Api\Http\Response\Factory 184 | */ 185 | protected function response() 186 | { 187 | return app(Factory::class); 188 | } 189 | 190 | /** 191 | * Magically handle calls to certain properties. 192 | * 193 | * @param string $key 194 | * @return mixed 195 | * 196 | * @throws \ErrorException 197 | */ 198 | public function __get($key) 199 | { 200 | $callable = [ 201 | 'api', 'user', 'auth', 'response', 202 | ]; 203 | 204 | if (in_array($key, $callable) && method_exists($this, $key)) { 205 | return $this->$key(); 206 | } 207 | 208 | throw new ErrorException('Undefined property '.get_class($this).'::'.$key); 209 | } 210 | 211 | /** 212 | * Magically handle calls to certain methods on the response factory. 213 | * 214 | * @param string $method 215 | * @param array $parameters 216 | * @return \Dingo\Api\Http\Response 217 | * 218 | * @throws \ErrorException 219 | */ 220 | public function __call($method, $parameters) 221 | { 222 | if (method_exists($this->response(), $method) || $method == 'array') { 223 | return call_user_func_array([$this->response(), $method], $parameters); 224 | } 225 | 226 | throw new ErrorException('Undefined method '.get_class($this).'::'.$method); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/Provider/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->configure('api'); 35 | 36 | $reflection = new ReflectionClass($this->app); 37 | 38 | $this->app[Request::class]->mergeMiddlewares( 39 | $this->gatherAppMiddleware($reflection) 40 | ); 41 | 42 | $this->addRequestMiddlewareToBeginning($reflection); 43 | 44 | // Because Lumen sets the route resolver at a very weird point we're going to 45 | // have to use reflection whenever the request instance is rebound to 46 | // set the route resolver to get the current route. 47 | $this->app->rebinding(IlluminateRequest::class, function ($app, $request) { 48 | $request->setRouteResolver(function () use ($app) { 49 | $reflection = new ReflectionClass($app); 50 | 51 | $property = $reflection->getProperty('currentRoute'); 52 | $property->setAccessible(true); 53 | 54 | return $property->getValue($app); 55 | }); 56 | }); 57 | 58 | // Validate FormRequest after resolving 59 | $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) { 60 | $resolved->validateResolved(); 61 | }); 62 | 63 | $this->app->resolving(FormRequest::class, function (FormRequest $request, Application $app) { 64 | $this->initializeRequest($request, $app['request']); 65 | 66 | $request->setContainer($app)->setRedirector($app->make(Redirector::class)); 67 | }); 68 | 69 | $this->app->routeMiddleware([ 70 | 'api.auth' => Auth::class, 71 | 'api.throttle' => RateLimit::class, 72 | 'api.controllers' => PrepareController::class, 73 | ]); 74 | } 75 | 76 | /** 77 | * Setup the configuration. 78 | * 79 | * @return void 80 | */ 81 | protected function setupConfig() 82 | { 83 | $this->app->configure('api'); 84 | 85 | parent::setupConfig(); 86 | } 87 | 88 | /** 89 | * Register the service provider. 90 | * 91 | * @return void 92 | */ 93 | public function register() 94 | { 95 | parent::register(); 96 | 97 | $this->app->singleton('api.router.adapter', function ($app) { 98 | return new LumenAdapter($app, new StdRouteParser, new GcbDataGenerator, $this->getDispatcherResolver()); 99 | }); 100 | } 101 | 102 | /** 103 | * Get the dispatcher resolver callback. 104 | * 105 | * @return \Closure 106 | */ 107 | protected function getDispatcherResolver() 108 | { 109 | return function ($routeCollector) { 110 | return new GroupCountBased($routeCollector->getData()); 111 | }; 112 | } 113 | 114 | /** 115 | * Add the request middleware to the beginning of the middleware stack on the 116 | * Lumen application instance. 117 | * 118 | * @param \ReflectionClass $reflection 119 | * @return void 120 | */ 121 | protected function addRequestMiddlewareToBeginning(ReflectionClass $reflection) 122 | { 123 | $property = $reflection->getProperty('middleware'); 124 | $property->setAccessible(true); 125 | 126 | $middleware = $property->getValue($this->app); 127 | 128 | array_unshift($middleware, Request::class); 129 | 130 | $property->setValue($this->app, $middleware); 131 | $property->setAccessible(false); 132 | } 133 | 134 | /** 135 | * Gather the application middleware besides this one so that we can send 136 | * our request through them, exactly how the developer wanted. 137 | * 138 | * @param \ReflectionClass $reflection 139 | * @return array 140 | */ 141 | protected function gatherAppMiddleware(ReflectionClass $reflection) 142 | { 143 | $property = $reflection->getProperty('middleware'); 144 | $property->setAccessible(true); 145 | 146 | $middleware = $property->getValue($this->app); 147 | 148 | return $middleware; 149 | } 150 | 151 | /** 152 | * Initialize the form request with data from the given request. 153 | * 154 | * @param FormRequest $form 155 | * @param IlluminateRequest $current 156 | * @return void 157 | */ 158 | protected function initializeRequest(FormRequest $form, IlluminateRequest $current) 159 | { 160 | $files = $current->files->all(); 161 | 162 | $files = is_array($files) ? array_filter($files) : $files; 163 | 164 | $form->initialize( 165 | $current->query->all(), 166 | $current->request->all(), 167 | $current->attributes->all(), 168 | $current->cookies->all(), 169 | $files, 170 | $current->server->all(), 171 | $current->getContent() 172 | ); 173 | 174 | $form->setJson($current->json()); 175 | 176 | try { 177 | if ($session = $current->getSession()) { 178 | $form->setLaravelSession($current->getSession()); 179 | } 180 | } catch (SessionNotFoundException $exception) { 181 | } 182 | 183 | $form->setUserResolver($current->getUserResolver()); 184 | 185 | $form->setRouteResolver($current->getRouteResolver()); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Http/Response/Format/JsonOptionalFormatting.php: -------------------------------------------------------------------------------- 1 | "\t", 33 | 'space' => ' ', 34 | ]; 35 | 36 | /* 37 | * JSON constants, that are allowed to be used as options while encoding. 38 | * Whitelist can be extended by other options in the future. 39 | * 40 | * @see http://php.net/manual/ru/json.constants.php 41 | * 42 | * @var array 43 | */ 44 | protected $jsonEncodeOptionsWhitelist = [ 45 | JSON_PRETTY_PRINT, 46 | JSON_UNESCAPED_UNICODE, 47 | ]; 48 | 49 | /** 50 | * Determine if JSON pretty print option is set to true. 51 | * 52 | * @return bool 53 | */ 54 | protected function isJsonPrettyPrintEnabled() 55 | { 56 | return isset($this->options['pretty_print']) && $this->options['pretty_print'] === true; 57 | } 58 | 59 | /** 60 | * Determine if JSON custom indent style is set. 61 | * 62 | * @return bool 63 | */ 64 | protected function isCustomIndentStyleRequired() 65 | { 66 | return $this->isJsonPrettyPrintEnabled() && 67 | isset($this->options['indent_style']) && 68 | in_array($this->options['indent_style'], $this->indentStyles); 69 | } 70 | 71 | /** 72 | * Perform JSON encode. 73 | * 74 | * @param string $content 75 | * @param array $jsonEncodeOptions 76 | * @return string 77 | */ 78 | protected function performJsonEncoding($content, array $jsonEncodeOptions = []) 79 | { 80 | $jsonEncodeOptions = $this->filterJsonEncodeOptions($jsonEncodeOptions); 81 | 82 | $optionsBitmask = $this->calucateJsonEncodeOptionsBitmask($jsonEncodeOptions); 83 | 84 | if (($encodedString = json_encode($content, $optionsBitmask)) === false) { 85 | throw new \ErrorException('Error encoding data in JSON format: '.json_last_error()); 86 | } 87 | 88 | return $encodedString; 89 | } 90 | 91 | /** 92 | * Filter JSON encode options array against the whitelist array. 93 | * 94 | * @param array $jsonEncodeOptions 95 | * @return array 96 | */ 97 | protected function filterJsonEncodeOptions(array $jsonEncodeOptions) 98 | { 99 | return array_intersect($jsonEncodeOptions, $this->jsonEncodeOptionsWhitelist); 100 | } 101 | 102 | /** 103 | * Sweep JSON encode options together to get options' bitmask. 104 | * 105 | * @param array $jsonEncodeOptions 106 | * @return int 107 | */ 108 | protected function calucateJsonEncodeOptionsBitmask(array $jsonEncodeOptions) 109 | { 110 | return array_sum($jsonEncodeOptions); 111 | } 112 | 113 | /** 114 | * Indent pretty printed JSON string, using given indent style. 115 | * 116 | * @param string $jsonString 117 | * @param string $indentStyle 118 | * @param int $defaultIndentSize 119 | * @return string 120 | */ 121 | protected function indentPrettyPrintedJson($jsonString, $indentStyle, $defaultIndentSize = 2) 122 | { 123 | $indentChar = $this->getIndentCharForIndentStyle($indentStyle); 124 | $indentSize = $this->getPrettyPrintIndentSize() ?: $defaultIndentSize; 125 | 126 | // If the given indentation style is allowed to have various indent size 127 | // (number of chars, that are used to indent one level in each line), 128 | // indent the JSON string with given (or default) indent size. 129 | if ($this->hasVariousIndentSize($indentStyle)) { 130 | return $this->peformIndentation($jsonString, $indentChar, $indentSize); 131 | } 132 | 133 | // Otherwise following the convention, that indent styles, that does not 134 | // allowed to have various indent size (e.g. tab) are indented using 135 | // one tabulation character per one indent level in each line. 136 | return $this->peformIndentation($jsonString, $indentChar); 137 | } 138 | 139 | /** 140 | * Get indent char for given indent style. 141 | * 142 | * @param string $indentStyle 143 | * @return string 144 | */ 145 | protected function getIndentCharForIndentStyle($indentStyle) 146 | { 147 | return $this->indentChars[$indentStyle]; 148 | } 149 | 150 | /** 151 | * Get indent size for pretty printed JSON string. 152 | * 153 | * @return int|null 154 | */ 155 | protected function getPrettyPrintIndentSize() 156 | { 157 | return isset($this->options['indent_size']) 158 | ? (int) $this->options['indent_size'] 159 | : null; 160 | } 161 | 162 | /** 163 | * Determine if indent style is allowed to have various indent size. 164 | * 165 | * @param string $indentStyle 166 | * @return bool 167 | */ 168 | protected function hasVariousIndentSize($indentStyle) 169 | { 170 | return in_array($indentStyle, $this->hasVariousIndentSize); 171 | } 172 | 173 | /** 174 | * Perform indentation for pretty printed JSON string with a given 175 | * indent char, repeated N times, as determined by indent size. 176 | * 177 | * @param string $jsonString JSON string, which must be indented 178 | * @param string $indentChar Char, used for indent (default is tab) 179 | * @param int $indentSize Number of times to repeat indent char per one indent level 180 | * @param int $defaultSpaces Default number of indent spaces after json_encode() 181 | * @return string 182 | */ 183 | protected function peformIndentation($jsonString, $indentChar = "\t", $indentSize = 1, $defaultSpaces = 4) 184 | { 185 | $pattern = '/(^|\G) {'.$defaultSpaces.'}/m'; 186 | $replacement = str_repeat($indentChar, $indentSize).'$1'; 187 | 188 | return preg_replace($pattern, $replacement, $jsonString); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Console/Command/Docs.php: -------------------------------------------------------------------------------- 1 | router = $router; 91 | $this->blueprint = $blueprint; 92 | $this->writer = $writer; 93 | $this->name = $name; 94 | $this->version = $version; 95 | } 96 | 97 | /** 98 | * Execute the console command. 99 | * 100 | * @return mixed 101 | */ 102 | public function handle() 103 | { 104 | $contents = $this->blueprint->generate($this->getControllers(), $this->getDocName(), $this->getVersion(), $this->getIncludePath()); 105 | 106 | if ($file = $this->option('output-file')) { 107 | $this->writer->write($contents, $file); 108 | 109 | return $this->info('Documentation was generated successfully.'); 110 | } 111 | 112 | return $this->line($contents); 113 | } 114 | 115 | /** 116 | * Get the documentation name. 117 | * 118 | * @return string 119 | */ 120 | protected function getDocName() 121 | { 122 | $name = $this->option('name') ?: $this->name; 123 | 124 | if (! $name) { 125 | $this->comment('A name for the documentation was not supplied. Use the --name option or set a default in the configuration.'); 126 | 127 | exit; 128 | } 129 | 130 | return $name; 131 | } 132 | 133 | /** 134 | * Get the include path for documentation files. 135 | * 136 | * @return string 137 | */ 138 | protected function getIncludePath() 139 | { 140 | return base_path($this->option('include-path')); 141 | } 142 | 143 | /** 144 | * Get the documentation version. 145 | * 146 | * @return string 147 | */ 148 | protected function getVersion() 149 | { 150 | $version = $this->option('use-version') ?: $this->version; 151 | 152 | if (! $version) { 153 | $this->comment('A version for the documentation was not supplied. Use the --use-version option or set a default in the configuration.'); 154 | 155 | exit; 156 | } 157 | 158 | return $version; 159 | } 160 | 161 | /** 162 | * Get all the controller instances. 163 | * 164 | * @return array 165 | */ 166 | protected function getControllers() 167 | { 168 | $controllers = new Collection; 169 | 170 | if ($controller = $this->option('use-controller')) { 171 | $this->addControllerIfNotExists($controllers, app($controller)); 172 | 173 | return $controllers; 174 | } 175 | 176 | foreach ($this->router->getRoutes() as $collections) { 177 | foreach ($collections as $route) { 178 | if ($controller = $route->getControllerInstance()) { 179 | $this->addControllerIfNotExists($controllers, $controller); 180 | } 181 | } 182 | } 183 | 184 | return $controllers; 185 | } 186 | 187 | /** 188 | * Add a controller to the collection if it does not exist. If the 189 | * controller implements an interface suffixed with "Docs" it 190 | * will be used instead of the controller. 191 | * 192 | * @param \Illuminate\Support\Collection $controllers 193 | * @param object $controller 194 | * @return void 195 | */ 196 | protected function addControllerIfNotExists(Collection $controllers, $controller) 197 | { 198 | $class = get_class($controller); 199 | 200 | if ($controllers->has($class)) { 201 | return; 202 | } 203 | 204 | $reflection = new ReflectionClass($controller); 205 | 206 | $interface = Arr::first($reflection->getInterfaces(), function ($key, $value) { 207 | return Str::endsWith($key, 'Docs'); 208 | }); 209 | 210 | if ($interface) { 211 | $controller = $interface; 212 | } 213 | 214 | $controllers->put($class, $controller); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Auth/Auth.php: -------------------------------------------------------------------------------- 1 | router = $router; 59 | $this->container = $container; 60 | $this->providers = $providers; 61 | } 62 | 63 | /** 64 | * Authenticate the current request. 65 | * 66 | * @param array $providers 67 | * @return mixed 68 | * 69 | * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException 70 | */ 71 | public function authenticate(array $providers = []) 72 | { 73 | $exceptionStack = []; 74 | 75 | // Spin through each of the registered authentication providers and attempt to 76 | // authenticate through one of them. This allows a developer to implement 77 | // and allow a number of different authentication mechanisms. 78 | foreach ($this->filterProviders($providers) as $provider) { 79 | try { 80 | $user = $provider->authenticate($this->router->getCurrentRequest(), $this->router->getCurrentRoute()); 81 | 82 | $this->providerUsed = $provider; 83 | 84 | return $this->user = $user; 85 | } catch (UnauthorizedHttpException $exception) { 86 | $exceptionStack[] = $exception; 87 | } catch (BadRequestHttpException $exception) { 88 | // We won't add this exception to the stack as it's thrown when the provider 89 | // is unable to authenticate due to the correct authorization header not 90 | // being set. We will throw an exception for this below. 91 | } 92 | } 93 | 94 | $this->throwUnauthorizedException($exceptionStack); 95 | } 96 | 97 | /** 98 | * Throw the first exception from the exception stack. 99 | * 100 | * @param array $exceptionStack 101 | * @return void 102 | * 103 | * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException 104 | */ 105 | protected function throwUnauthorizedException(array $exceptionStack) 106 | { 107 | $exception = array_shift($exceptionStack); 108 | 109 | if ($exception === null) { 110 | $exception = new UnauthorizedHttpException('dingo', 'Failed to authenticate because of bad credentials or an invalid authorization header.'); 111 | } 112 | 113 | throw $exception; 114 | } 115 | 116 | /** 117 | * Filter the requested providers from the available providers. 118 | * 119 | * @param array $providers 120 | * @return array 121 | */ 122 | protected function filterProviders(array $providers) 123 | { 124 | if (empty($providers)) { 125 | return $this->providers; 126 | } 127 | 128 | return array_intersect_key($this->providers, array_flip($providers)); 129 | } 130 | 131 | /** 132 | * Get the authenticated user. 133 | * 134 | * @param bool $authenticate 135 | * @return \Illuminate\Auth\GenericUser|\Illuminate\Database\Eloquent\Model|null 136 | */ 137 | public function getUser($authenticate = true) 138 | { 139 | if ($this->user) { 140 | return $this->user; 141 | } elseif (! $authenticate) { 142 | return; 143 | } 144 | 145 | try { 146 | return $this->user = $this->authenticate(); 147 | } catch (Exception $exception) { 148 | return; 149 | } 150 | } 151 | 152 | /** 153 | * Alias for getUser. 154 | * 155 | * @param bool $authenticate 156 | * @return \Illuminate\Auth\GenericUser|\Illuminate\Database\Eloquent\Model 157 | */ 158 | public function user($authenticate = true) 159 | { 160 | return $this->getUser($authenticate); 161 | } 162 | 163 | /** 164 | * Set the authenticated user. 165 | * 166 | * @param \Illuminate\Auth\GenericUser|\Illuminate\Database\Eloquent\Model $user 167 | * @return \Dingo\Api\Auth\Auth 168 | */ 169 | public function setUser($user) 170 | { 171 | $this->user = $user; 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Check if a user has authenticated with the API. 178 | * 179 | * @param bool $authenticate 180 | * @return bool 181 | */ 182 | public function check($authenticate = false) 183 | { 184 | return ! is_null($this->user($authenticate)); 185 | } 186 | 187 | /** 188 | * Get the provider used for authentication. 189 | * 190 | * @return \Dingo\Api\Contract\Auth\Provider 191 | */ 192 | public function getProviderUsed() 193 | { 194 | return $this->providerUsed; 195 | } 196 | 197 | /** 198 | * Extend the authentication layer with a custom provider. 199 | * 200 | * @param string $key 201 | * @param object|callable $provider 202 | * @return void 203 | */ 204 | public function extend($key, $provider) 205 | { 206 | if (is_callable($provider)) { 207 | $provider = call_user_func($provider, $this->container); 208 | } 209 | 210 | $this->providers[$key] = $provider; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Transformer/Factory.php: -------------------------------------------------------------------------------- 1 | container = $container; 47 | $this->adapter = $adapter; 48 | } 49 | 50 | /** 51 | * Register a transformer binding resolver for a class. 52 | * 53 | * @param $class 54 | * @param $resolver 55 | * @param array $parameters 56 | * @param \Closure|null $after 57 | * @return \Dingo\Api\Transformer\Binding 58 | */ 59 | public function register($class, $resolver, array $parameters = [], ?Closure $after = null) 60 | { 61 | return $this->bindings[$class] = $this->createBinding($resolver, $parameters, $after); 62 | } 63 | 64 | /** 65 | * Transform a response. 66 | * 67 | * @param string|object $response 68 | * @return mixed 69 | */ 70 | public function transform($response) 71 | { 72 | $binding = $this->getBinding($response); 73 | 74 | return $this->adapter->transform($response, $binding->resolveTransformer(), $binding, $this->getRequest()); 75 | } 76 | 77 | /** 78 | * Determine if a response is transformable. 79 | * 80 | * @param mixed $response 81 | * @return bool 82 | */ 83 | public function transformableResponse($response) 84 | { 85 | return $this->transformableType($response) && $this->hasBinding($response); 86 | } 87 | 88 | /** 89 | * Determine if a value is of a transformable type. 90 | * 91 | * @param mixed $value 92 | * @return bool 93 | */ 94 | public function transformableType($value) 95 | { 96 | return is_object($value) || is_string($value); 97 | } 98 | 99 | /** 100 | * Get a registered transformer binding. 101 | * 102 | * @param string|object $class 103 | * @return \Dingo\Api\Transformer\Binding 104 | * 105 | * @throws \RuntimeException 106 | */ 107 | public function getBinding($class) 108 | { 109 | if ($this->isCollection($class) && ! $class->isEmpty()) { 110 | return $this->getBindingFromCollection($class); 111 | } 112 | 113 | $class = is_object($class) ? get_class($class) : $class; 114 | 115 | if (! $this->hasBinding($class)) { 116 | throw new RuntimeException('Unable to find bound transformer for "'.$class.'" class.'); 117 | } 118 | 119 | return $this->bindings[$class]; 120 | } 121 | 122 | /** 123 | * Create a new binding instance. 124 | * 125 | * @param string|callable|object $resolver 126 | * @param array $parameters 127 | * @param \Closure|null $callback 128 | * @return \Dingo\Api\Transformer\Binding 129 | */ 130 | protected function createBinding($resolver, array $parameters = [], ?Closure $callback = null) 131 | { 132 | return new Binding($this->container, $resolver, $parameters, $callback); 133 | } 134 | 135 | /** 136 | * Get a registered transformer binding from a collection of items. 137 | * 138 | * @param \Illuminate\Support\Collection $collection 139 | * @return null|string|callable 140 | */ 141 | protected function getBindingFromCollection($collection) 142 | { 143 | return $this->getBinding($collection->first()); 144 | } 145 | 146 | /** 147 | * Determine if a class has a transformer binding. 148 | * 149 | * @param string|object $class 150 | * @return bool 151 | */ 152 | protected function hasBinding($class) 153 | { 154 | if ($this->isCollection($class) && ! $class->isEmpty()) { 155 | $class = $class->first(); 156 | } 157 | 158 | $class = is_object($class) ? get_class($class) : $class; 159 | 160 | return isset($this->bindings[$class]); 161 | } 162 | 163 | /** 164 | * Determine if the instance is a collection. 165 | * 166 | * @param object $instance 167 | * @return bool 168 | */ 169 | protected function isCollection($instance) 170 | { 171 | return $instance instanceof Collection || $instance instanceof Paginator; 172 | } 173 | 174 | /** 175 | * Get the array of registered transformer bindings. 176 | * 177 | * @return array 178 | */ 179 | public function getTransformerBindings() 180 | { 181 | return $this->bindings; 182 | } 183 | 184 | /** 185 | * Set the transformation layer at runtime. 186 | * 187 | * @param \Closure|\Dingo\Api\Contract\Transformer\Adapter $adapter 188 | * @return void 189 | */ 190 | public function setAdapter($adapter) 191 | { 192 | if (is_callable($adapter)) { 193 | $adapter = call_user_func($adapter, $this->container); 194 | } 195 | 196 | $this->adapter = $adapter; 197 | } 198 | 199 | /** 200 | * Get the transformation layer adapter. 201 | * 202 | * @return \Dingo\Api\Contract\Transformer\Adapter 203 | */ 204 | public function getAdapter() 205 | { 206 | return $this->adapter; 207 | } 208 | 209 | /** 210 | * Get the request from the container. 211 | * 212 | * @return \Dingo\Api\Http\Request 213 | */ 214 | public function getRequest() 215 | { 216 | $request = $this->container['request']; 217 | 218 | if ($request instanceof IlluminateRequest && ! $request instanceof Request) { 219 | $request = (new Request())->createFromIlluminate($request); 220 | } 221 | 222 | return $request; 223 | } 224 | 225 | /** 226 | * Pass unknown method calls through to the adapter. 227 | * 228 | * @param string $method 229 | * @param array $parameters 230 | * @return mixed 231 | */ 232 | public function __call($method, $parameters) 233 | { 234 | return call_user_func_array([$this->adapter, $method], $parameters); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Provider/LaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([realpath(__DIR__.'/../../config/api.php') => config_path('api.php')]); 33 | 34 | $kernel = $this->app->make(Kernel::class); 35 | 36 | $this->app[Request::class]->mergeMiddlewares( 37 | $this->gatherAppMiddleware($kernel) 38 | ); 39 | 40 | $this->addRequestMiddlewareToBeginning($kernel); 41 | 42 | $this->app['events']->listen(RequestWasMatched::class, function (RequestWasMatched $event) { 43 | $this->replaceRouteDispatcher(); 44 | 45 | $this->updateRouterBindings(); 46 | }); 47 | 48 | // Originally Validate FormRequest after resolving 49 | /* This casues the prepareForValidation() function to be called twice, and seemingly has no other benefit, see discussion at 50 | https://github.com/dingo/api/issues/1668 51 | This is already done by laravel service provider, and works with Dingo router 52 | $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) { 53 | $resolved->validateResolved(); 54 | }); 55 | */ 56 | 57 | $this->app->resolving(FormRequest::class, function (FormRequest $request, Application $app) { 58 | $this->initializeRequest($request, $app['request']); 59 | 60 | $request->setContainer($app)->setRedirector($app->make(Redirector::class)); 61 | }); 62 | 63 | $this->addMiddlewareAlias('api.auth', Auth::class); 64 | $this->addMiddlewareAlias('api.throttle', RateLimit::class); 65 | $this->addMiddlewareAlias('api.controllers', PrepareController::class); 66 | } 67 | 68 | /** 69 | * Replace the route dispatcher. 70 | * 71 | * @return void 72 | */ 73 | protected function replaceRouteDispatcher() 74 | { 75 | $this->app->singleton('illuminate.route.dispatcher', function ($app) { 76 | return new ControllerDispatcher($app['api.router.adapter']->getRouter(), $app); 77 | }); 78 | } 79 | 80 | /** 81 | * Grab the bindings from the Laravel router and set them on the adapters 82 | * router. 83 | * 84 | * @return void 85 | */ 86 | protected function updateRouterBindings() 87 | { 88 | foreach ($this->getRouterBindings() as $key => $binding) { 89 | $this->app['api.router.adapter']->getRouter()->bind($key, $binding); 90 | } 91 | } 92 | 93 | /** 94 | * Get the Laravel routers bindings. 95 | * 96 | * @return array 97 | */ 98 | protected function getRouterBindings() 99 | { 100 | $property = (new ReflectionClass($this->app['router']))->getProperty('binders'); 101 | $property->setAccessible(true); 102 | 103 | return $property->getValue($this->app['router']); 104 | } 105 | 106 | /** 107 | * Register the service provider. 108 | * 109 | * @return void 110 | */ 111 | public function register() 112 | { 113 | parent::register(); 114 | 115 | $this->registerRouterAdapter(); 116 | } 117 | 118 | /** 119 | * Register the router adapter. 120 | * 121 | * @return void 122 | */ 123 | protected function registerRouterAdapter() 124 | { 125 | $this->app->singleton('api.router.adapter', function ($app) { 126 | return new LaravelAdapter($app['router']); 127 | }); 128 | } 129 | 130 | /** 131 | * Add the request middleware to the beginning of the kernel. 132 | * 133 | * @param \Illuminate\Contracts\Http\Kernel $kernel 134 | * @return void 135 | */ 136 | protected function addRequestMiddlewareToBeginning(Kernel $kernel) 137 | { 138 | $kernel->prependMiddleware(Request::class); 139 | } 140 | 141 | /** 142 | * Register a short-hand name for a middleware. For compatibility 143 | * with Laravel < 5.4 check if aliasMiddleware exists since this 144 | * method has been renamed. 145 | * 146 | * @param string $name 147 | * @param string $class 148 | * @return void 149 | */ 150 | protected function addMiddlewareAlias($name, $class) 151 | { 152 | $router = $this->app['router']; 153 | 154 | if (method_exists($router, 'aliasMiddleware')) { 155 | return $router->aliasMiddleware($name, $class); 156 | } 157 | 158 | return $router->middleware($name, $class); 159 | } 160 | 161 | /** 162 | * Gather the application middleware besides this one so that we can send 163 | * our request through them, exactly how the developer wanted. 164 | * 165 | * @param \Illuminate\Contracts\Http\Kernel $kernel 166 | * @return array 167 | */ 168 | protected function gatherAppMiddleware(Kernel $kernel) 169 | { 170 | $property = (new ReflectionClass($kernel))->getProperty('middleware'); 171 | $property->setAccessible(true); 172 | 173 | return $property->getValue($kernel); 174 | } 175 | 176 | /** 177 | * Initialize the form request with data from the given request. 178 | * 179 | * @param FormRequest $form 180 | * @param IlluminateRequest $current 181 | * @return void 182 | */ 183 | protected function initializeRequest(FormRequest $form, IlluminateRequest $current) 184 | { 185 | $files = $current->files->all(); 186 | 187 | $files = is_array($files) ? array_filter($files) : $files; 188 | 189 | $form->initialize( 190 | $current->query->all(), 191 | $current->request->all(), 192 | $current->attributes->all(), 193 | $current->cookies->all(), 194 | $files, 195 | $current->server->all(), 196 | $current->getContent() 197 | ); 198 | 199 | $form->setJson($current->json()); 200 | 201 | try { 202 | if ($session = $current->getSession()) { 203 | $form->setLaravelSession($current->getSession()); 204 | } 205 | } catch (SessionNotFoundException $exception) { 206 | } 207 | 208 | $form->setUserResolver($current->getUserResolver()); 209 | 210 | $form->setRouteResolver($current->getRouteResolver()); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Provider/DingoServiceProvider.php: -------------------------------------------------------------------------------- 1 | setResponseStaticInstances(); 26 | 27 | Request::setAcceptParser($this->app[\Dingo\Api\Http\Parser\Accept::class]); 28 | 29 | $this->app->rebinding('api.routes', function ($app, $routes) { 30 | $app['api.url']->setRouteCollections($routes); 31 | }); 32 | } 33 | 34 | protected function setResponseStaticInstances() 35 | { 36 | Response::setFormatters($this->config('formats')); 37 | Response::setFormatsOptions($this->config('formatsOptions')); 38 | Response::setTransformer($this->app['api.transformer']); 39 | Response::setEventDispatcher($this->app['events']); 40 | } 41 | 42 | /** 43 | * Register the service provider. 44 | * 45 | * @return void 46 | */ 47 | public function register() 48 | { 49 | $this->registerConfig(); 50 | 51 | $this->registerClassAliases(); 52 | 53 | $this->app->register(RoutingServiceProvider::class); 54 | 55 | $this->app->register(HttpServiceProvider::class); 56 | 57 | $this->registerExceptionHandler(); 58 | 59 | $this->registerDispatcher(); 60 | $this->registerCallableDispatcher(); 61 | 62 | $this->registerAuth(); 63 | 64 | $this->registerTransformer(); 65 | 66 | // $this->registerDocsCommand(); 67 | 68 | if (class_exists('Illuminate\Foundation\Application', false)) { 69 | $this->commands([ 70 | \Dingo\Api\Console\Command\Cache::class, 71 | \Dingo\Api\Console\Command\Routes::class, 72 | ]); 73 | } 74 | } 75 | 76 | /** 77 | * Register the configuration. 78 | * 79 | * @return void 80 | */ 81 | protected function registerConfig() 82 | { 83 | $this->mergeConfigFrom(realpath(__DIR__.'/../../config/api.php'), 'api'); 84 | 85 | if (! $this->app->runningInConsole() && empty($this->config('prefix')) && empty($this->config('domain'))) { 86 | throw new RuntimeException('Unable to boot ApiServiceProvider, configure an API domain or prefix.'); 87 | } 88 | } 89 | 90 | /** 91 | * Register the class aliases. 92 | * 93 | * @return void 94 | */ 95 | protected function registerClassAliases() 96 | { 97 | $serviceAliases = [ 98 | \Dingo\Api\Http\Request::class => \Dingo\Api\Contract\Http\Request::class, 99 | 'api.dispatcher' => \Dingo\Api\Dispatcher::class, 100 | 'api.http.validator' => \Dingo\Api\Http\RequestValidator::class, 101 | 'api.http.response' => \Dingo\Api\Http\Response\Factory::class, 102 | 'api.router' => \Dingo\Api\Routing\Router::class, 103 | 'api.router.adapter' => \Dingo\Api\Contract\Routing\Adapter::class, 104 | 'api.auth' => \Dingo\Api\Auth\Auth::class, 105 | 'api.limiting' => \Dingo\Api\Http\RateLimit\Handler::class, 106 | 'api.transformer' => \Dingo\Api\Transformer\Factory::class, 107 | 'api.url' => \Dingo\Api\Routing\UrlGenerator::class, 108 | 'api.exception' => [\Dingo\Api\Exception\Handler::class, \Dingo\Api\Contract\Debug\ExceptionHandler::class], 109 | ]; 110 | 111 | foreach ($serviceAliases as $key => $aliases) { 112 | foreach ((array) $aliases as $alias) { 113 | $this->app->alias($key, $alias); 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * Register the exception handler. 120 | * 121 | * @return void 122 | */ 123 | protected function registerExceptionHandler() 124 | { 125 | $this->app->singleton('api.exception', function ($app) { 126 | return new ExceptionHandler($app['Illuminate\Contracts\Debug\ExceptionHandler'], $this->config('errorFormat'), $this->config('debug')); 127 | }); 128 | } 129 | 130 | /** 131 | * Register the internal dispatcher. 132 | * 133 | * @return void 134 | */ 135 | public function registerDispatcher() 136 | { 137 | $this->app->singleton('api.dispatcher', function ($app) { 138 | $dispatcher = new Dispatcher($app, $app['files'], $app[\Dingo\Api\Routing\Router::class], $app[\Dingo\Api\Auth\Auth::class]); 139 | 140 | $dispatcher->setSubtype($this->config('subtype')); 141 | $dispatcher->setStandardsTree($this->config('standardsTree')); 142 | $dispatcher->setPrefix($this->config('prefix')); 143 | $dispatcher->setDefaultVersion($this->config('version')); 144 | $dispatcher->setDefaultDomain($this->config('domain')); 145 | $dispatcher->setDefaultFormat($this->config('defaultFormat')); 146 | 147 | return $dispatcher; 148 | }); 149 | } 150 | 151 | public function registerCallableDispatcher() 152 | { 153 | $this->app->singleton(CallableDispatcherContract::class, function ($app) { 154 | return new CallableDispatcher($app); 155 | }); 156 | } 157 | 158 | /** 159 | * Register the auth. 160 | * 161 | * @return void 162 | */ 163 | protected function registerAuth() 164 | { 165 | $this->app->singleton('api.auth', function ($app) { 166 | return new Auth($app[\Dingo\Api\Routing\Router::class], $app, $this->config('auth')); 167 | }); 168 | } 169 | 170 | /** 171 | * Register the transformer factory. 172 | * 173 | * @return void 174 | */ 175 | protected function registerTransformer() 176 | { 177 | $this->app->singleton('api.transformer', function ($app) { 178 | return new TransformerFactory($app, $this->config('transformer')); 179 | }); 180 | } 181 | 182 | /** 183 | * Register the documentation command. 184 | * 185 | * @return void 186 | */ 187 | protected function registerDocsCommand() 188 | { 189 | if (class_exists(\Dingo\Blueprint\Blueprint::class)) { 190 | $this->app->singleton(\Dingo\Api\Console\Command\Docs::class, function ($app) { 191 | return new Command\Docs( 192 | $app[\Dingo\Api\Routing\Router::class], 193 | $app[\Dingo\Blueprint\Blueprint::class], 194 | $app[\Dingo\Blueprint\Writer::class], 195 | $this->config('name'), 196 | $this->config('version') 197 | ); 198 | }); 199 | 200 | $this->commands([\Dingo\Api\Console\Command\Docs::class]); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Http/Middleware/Request.php: -------------------------------------------------------------------------------- 1 | app = $app; 76 | $this->exception = $exception; 77 | $this->router = $router; 78 | $this->validator = $validator; 79 | $this->events = $events; 80 | } 81 | 82 | /** 83 | * Handle an incoming request. 84 | * 85 | * @param \Illuminate\Http\Request $request 86 | * @param \Closure $next 87 | * @return mixed 88 | */ 89 | public function handle($request, Closure $next) 90 | { 91 | try { 92 | if ($this->validator->validateRequest($request)) { 93 | $this->app->singleton(LaravelExceptionHandler::class, function ($app) { 94 | return $app[ExceptionHandler::class]; 95 | }); 96 | 97 | $request = $this->app->make(RequestContract::class)->createFromIlluminate($request); 98 | 99 | $this->events->dispatch(new RequestWasMatched($request, $this->app)); 100 | 101 | return $this->sendRequestThroughRouter($request); 102 | } 103 | } catch (Exception $exception) { 104 | $this->exception->report($exception); 105 | 106 | return $this->exception->handle($exception); 107 | } 108 | 109 | return $next($request); 110 | } 111 | 112 | /** 113 | * Send the request through the Dingo router. 114 | * 115 | * @param \Dingo\Api\Http\Request $request 116 | * @return \Dingo\Api\Http\Response 117 | */ 118 | protected function sendRequestThroughRouter(HttpRequest $request) 119 | { 120 | $this->app->instance('request', $request); 121 | 122 | return (new Pipeline($this->app))->send($request)->through($this->middleware)->then(function ($request) { 123 | return $this->router->dispatch($request); 124 | }); 125 | } 126 | 127 | /** 128 | * Call the terminate method on middlewares. 129 | * 130 | * @return void 131 | */ 132 | public function terminate($request, $response) 133 | { 134 | if (! ($request = $this->app['request']) instanceof HttpRequest) { 135 | return; 136 | } 137 | 138 | // Laravel's route middlewares can be terminated just like application 139 | // middleware, so we'll gather all the route middleware here. 140 | // On Lumen this will simply be an empty array as it does 141 | // not implement terminable route middleware. 142 | $middlewares = $this->gatherRouteMiddlewares($request); 143 | 144 | // Because of how middleware is executed on Lumen we'll need to merge in the 145 | // application middlewares now so that we can terminate them. Laravel does 146 | // not need this as it handles things a little more gracefully so it 147 | // can terminate the application ones itself. 148 | if (class_exists(Application::class, false)) { 149 | $middlewares = array_merge($middlewares, $this->middleware); 150 | } 151 | 152 | foreach ($middlewares as $middleware) { 153 | if ($middleware instanceof Closure) { 154 | continue; 155 | } 156 | 157 | [$name, $parameters] = $this->parseMiddleware($middleware); 158 | 159 | $instance = $this->app->make($name); 160 | 161 | if (method_exists($instance, 'terminate')) { 162 | $instance->terminate($request, $response); 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Parse a middleware string to get the name and parameters. 169 | * 170 | * @author Taylor Otwell 171 | * 172 | * @param string $middleware 173 | * @return array 174 | */ 175 | protected function parseMiddleware($middleware) 176 | { 177 | [$name, $parameters] = array_pad(explode(':', $middleware, 2), 2, []); 178 | 179 | if (is_string($parameters)) { 180 | $parameters = explode(',', $parameters); 181 | } 182 | 183 | return [$name, $parameters]; 184 | } 185 | 186 | /** 187 | * Gather the middlewares for the route. 188 | * 189 | * @param \Dingo\Api\Http\Request $request 190 | * @return array 191 | */ 192 | protected function gatherRouteMiddlewares($request) 193 | { 194 | if ($route = $request->route()) { 195 | return $this->router->gatherRouteMiddlewares($route); 196 | } 197 | 198 | return []; 199 | } 200 | 201 | /** 202 | * Set the middlewares. 203 | * 204 | * @param array $middleware 205 | * @return void 206 | */ 207 | public function setMiddlewares(array $middleware) 208 | { 209 | $this->middleware = $middleware; 210 | } 211 | 212 | /** 213 | * Merge new middlewares onto the existing middlewares. 214 | * 215 | * @param array $middleware 216 | * @return void 217 | */ 218 | public function mergeMiddlewares(array $middleware) 219 | { 220 | $this->middleware = array_merge($this->middleware, $middleware); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Routing/Adapter/Laravel.php: -------------------------------------------------------------------------------- 1 | router = $router; 65 | } 66 | 67 | /** 68 | * Dispatch a request. 69 | * 70 | * @param \Illuminate\Http\Request $request 71 | * @param string $version 72 | * @return mixed 73 | */ 74 | public function dispatch(Request $request, $version) 75 | { 76 | if (! isset($this->routes[$version])) { 77 | throw new UnknownVersionException; 78 | } 79 | 80 | $routes = $this->mergeOldRoutes($version); 81 | 82 | $this->router->setRoutes($routes); 83 | 84 | $router = clone $this->router; 85 | 86 | $response = $router->dispatch($request); 87 | 88 | unset($router); 89 | 90 | return $response; 91 | } 92 | 93 | /** 94 | * Merge the old application routes with the API routes. 95 | * 96 | * @param string $version 97 | * @return array 98 | */ 99 | protected function mergeOldRoutes($version) 100 | { 101 | if (! isset($this->oldRoutes)) { 102 | $this->oldRoutes = $this->router->getRoutes(); 103 | } 104 | 105 | if (! isset($this->mergedRoutes[$version])) { 106 | $this->mergedRoutes[$version] = $this->routes[$version]; 107 | 108 | foreach ($this->oldRoutes as $route) { 109 | $this->mergedRoutes[$version]->add($route); 110 | } 111 | 112 | $this->mergedRoutes[$version]->refreshNameLookups(); 113 | $this->mergedRoutes[$version]->refreshActionLookups(); 114 | } 115 | 116 | return $this->mergedRoutes[$version]; 117 | } 118 | 119 | /** 120 | * Get the URI, methods, and action from the route. 121 | * 122 | * @param mixed $route 123 | * @param \Illuminate\Http\Request $request 124 | * @return array 125 | */ 126 | public function getRouteProperties($route, Request $request) 127 | { 128 | if (method_exists($route, 'uri') && method_exists($route, 'methods')) { 129 | return [$route->uri(), $route->methods(), $route->getAction()]; 130 | } 131 | 132 | return [$route->getUri(), $route->getMethods(), $route->getAction()]; 133 | } 134 | 135 | /** 136 | * Add a route to the appropriate route collection. 137 | * 138 | * @param array $methods 139 | * @param array $versions 140 | * @param string $uri 141 | * @param mixed $action 142 | * @return \Illuminate\Routing\Route 143 | */ 144 | public function addRoute(array $methods, array $versions, $uri, $action) 145 | { 146 | $this->createRouteCollections($versions); 147 | 148 | // Add where-patterns from original laravel router 149 | $action['where'] = array_merge($this->router->getPatterns(), $action['where'] ?? []); 150 | 151 | $route = new Route($methods, $uri, $action); 152 | $route->where($action['where']); 153 | 154 | foreach ($versions as $version) { 155 | $this->routes[$version]->add($route); 156 | } 157 | 158 | return $route; 159 | } 160 | 161 | /** 162 | * Create the route collections for the versions. 163 | * 164 | * @param array $versions 165 | * @return void 166 | */ 167 | protected function createRouteCollections(array $versions) 168 | { 169 | foreach ($versions as $version) { 170 | if (! isset($this->routes[$version])) { 171 | $this->routes[$version] = new RouteCollection; 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * Get all routes or only for a specific version. 178 | * 179 | * @param string $version 180 | * @return mixed 181 | */ 182 | public function getRoutes($version = null) 183 | { 184 | if (! is_null($version)) { 185 | return $this->routes[$version]; 186 | } 187 | 188 | return $this->routes; 189 | } 190 | 191 | /** 192 | * Get a normalized iterable set of routes. 193 | * 194 | * @param string $version 195 | * @return mixed 196 | */ 197 | public function getIterableRoutes($version = null) 198 | { 199 | return $this->getRoutes($version); 200 | } 201 | 202 | /** 203 | * Set the routes on the adapter. 204 | * 205 | * @param array $routes 206 | * @return void 207 | */ 208 | public function setRoutes(array $routes) 209 | { 210 | $this->routes = $routes; 211 | } 212 | 213 | /** 214 | * Prepare a route for serialization. 215 | * 216 | * @param mixed $route 217 | * @return mixed 218 | */ 219 | public function prepareRouteForSerialization($route) 220 | { 221 | $route->prepareForSerialization(); 222 | 223 | return $route; 224 | } 225 | 226 | /** 227 | * Gather the route middlewares. 228 | * 229 | * @param \Illuminate\Routing\Route $route 230 | * @return array 231 | */ 232 | public function gatherRouteMiddlewares($route) 233 | { 234 | if (method_exists($this->router, 'gatherRouteMiddleware')) { 235 | return $this->router->gatherRouteMiddleware($route); 236 | } 237 | 238 | return $this->router->gatherRouteMiddlewares($route); 239 | } 240 | 241 | /** 242 | * Get the Laravel router instance. 243 | * 244 | * @return \Illuminate\Routing\Router 245 | */ 246 | public function getRouter() 247 | { 248 | return $this->router; 249 | } 250 | 251 | /** 252 | * Get the global "where" patterns. 253 | * 254 | * @return array 255 | */ 256 | public function getPatterns() 257 | { 258 | return $this->patterns; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/Http/FormRequest.php: -------------------------------------------------------------------------------- 1 | authorize() === false) { 78 | throw new AccessDeniedHttpException(); 79 | } 80 | 81 | $validator = app('validator')->make($this->all(), $this->rules(), $this->messages()); 82 | 83 | if ($validator->fails()) { 84 | throw new ValidationHttpException($validator->errors()); 85 | } 86 | } 87 | 88 | /** 89 | * Get the validator instance for the request. 90 | * 91 | * @return \Illuminate\Contracts\Validation\Validator 92 | * 93 | * @SuppressWarnings(PHPMD.ElseExpression) 94 | */ 95 | protected function getValidatorInstance() 96 | { 97 | $factory = $this->container->make(ValidationFactory::class); 98 | 99 | if (method_exists($this, 'validator')) { 100 | $validator = $this->container->call([$this, 'validator'], compact('factory')); 101 | } else { 102 | $validator = $this->createDefaultValidator($factory); 103 | } 104 | 105 | if (method_exists($this, 'withValidator')) { 106 | $this->withValidator($validator); 107 | } 108 | 109 | return $validator; 110 | } 111 | 112 | /** 113 | * Create the default validator instance. 114 | * 115 | * @param \Illuminate\Contracts\Validation\Factory $factory 116 | * @return \Illuminate\Contracts\Validation\Validator 117 | */ 118 | protected function createDefaultValidator(ValidationFactory $factory) 119 | { 120 | return $factory->make( 121 | $this->validationData(), 122 | $this->container->call([$this, 'rules']), 123 | $this->messages(), 124 | $this->attributes() 125 | ); 126 | } 127 | 128 | /** 129 | * Get data to be validated from the request. 130 | * 131 | * @return array 132 | */ 133 | protected function validationData() 134 | { 135 | return $this->all(); 136 | } 137 | 138 | /** 139 | * Handle a failed validation attempt. 140 | * 141 | * @param \Illuminate\Contracts\Validation\Validator $validator 142 | * @return void 143 | */ 144 | protected function failedValidation(Validator $validator) 145 | { 146 | if ($this->container['request'] instanceof Request) { 147 | throw new ValidationHttpException($validator->errors()); 148 | } 149 | 150 | parent::failedValidation($validator); 151 | } 152 | 153 | /** 154 | * Get the proper failed validation response for the request. 155 | * 156 | * @param array $errors 157 | * @return \Symfony\Component\HttpFoundation\Response 158 | */ 159 | public function response(array $errors) 160 | { 161 | if ($this->expectsJson()) { 162 | return new JsonResponse($errors, 422); 163 | } 164 | 165 | return $this->redirector->to($this->getRedirectUrl()) 166 | ->withInput($this->except($this->dontFlash)) 167 | ->withErrors($errors, $this->errorBag); 168 | } 169 | 170 | /** 171 | * Format the errors from the given Validator instance. 172 | * 173 | * @param \Illuminate\Contracts\Validation\Validator $validator 174 | * @return array 175 | */ 176 | protected function formatErrors(Validator $validator) 177 | { 178 | return $validator->getMessageBag()->toArray(); 179 | } 180 | 181 | /** 182 | * Get the URL to redirect to on a validation error. 183 | * 184 | * @return string 185 | */ 186 | protected function getRedirectUrl() 187 | { 188 | $url = $this->redirector->getUrlGenerator(); 189 | 190 | if ($this->redirect) { 191 | return $url->to($this->redirect); 192 | } elseif ($this->redirectRoute) { 193 | return $url->route($this->redirectRoute); 194 | } elseif ($this->redirectAction) { 195 | return $url->action($this->redirectAction); 196 | } 197 | 198 | return $url->previous(); 199 | } 200 | 201 | /** 202 | * Determine if the request passes the authorization check. 203 | * 204 | * @return bool 205 | */ 206 | protected function passesAuthorization() 207 | { 208 | if (method_exists($this, 'authorize')) { 209 | return $this->container->call([$this, 'authorize']); 210 | } 211 | 212 | return false; 213 | } 214 | 215 | /** 216 | * Handle a failed authorization attempt. 217 | * 218 | * @return void 219 | */ 220 | protected function failedAuthorization() 221 | { 222 | if ($this->container['request'] instanceof Request) { 223 | throw new HttpException(403); 224 | } 225 | 226 | parent::failedAuthorization(); 227 | } 228 | 229 | /** 230 | * Get custom messages for validator errors. 231 | * 232 | * @return array 233 | */ 234 | public function messages() 235 | { 236 | return []; 237 | } 238 | 239 | /** 240 | * Get custom attributes for validator errors. 241 | * 242 | * @return array 243 | */ 244 | public function attributes() 245 | { 246 | return []; 247 | } 248 | 249 | /** 250 | * Set the Redirector instance. 251 | * 252 | * @param \Laravel\Lumen\Http\Redirector|\Illuminate\Routing\Redirector $redirector 253 | * @return $this 254 | */ 255 | public function setRedirector($redirector) 256 | { 257 | $this->redirector = $redirector; 258 | 259 | return $this; 260 | } 261 | 262 | /** 263 | * Set the container implementation. 264 | * 265 | * @param \Illuminate\Contracts\Container\Container $container 266 | * @return $this 267 | */ 268 | public function setContainer(Container $container) 269 | { 270 | $this->container = $container; 271 | 272 | return $this; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /config/api.php: -------------------------------------------------------------------------------- 1 | env('API_STANDARDS_TREE', 'x'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | API Subtype 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Your subtype will follow the standards tree you use when used in the 29 | | "Accept" header to negotiate the content type and version. 30 | | 31 | | For example: Accept: application/x.SUBTYPE.v1+json 32 | | 33 | */ 34 | 35 | 'subtype' => env('API_SUBTYPE', ''), 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Default API Version 40 | |-------------------------------------------------------------------------- 41 | | 42 | | This is the default version when strict mode is disabled and your API 43 | | is accessed via a web browser. It's also used as the default version 44 | | when generating your APIs documentation. 45 | | 46 | */ 47 | 48 | 'version' => env('API_VERSION', 'v1'), 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Default API Prefix 53 | |-------------------------------------------------------------------------- 54 | | 55 | | A default prefix to use for your API routes so you don't have to 56 | | specify it for each group. 57 | | 58 | */ 59 | 60 | 'prefix' => env('API_PREFIX', null), 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Default API Domain 65 | |-------------------------------------------------------------------------- 66 | | 67 | | A default domain to use for your API routes so you don't have to 68 | | specify it for each group. 69 | | 70 | */ 71 | 72 | 'domain' => env('API_DOMAIN', null), 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Name 77 | |-------------------------------------------------------------------------- 78 | | 79 | | When documenting your API using the API Blueprint syntax you can 80 | | configure a default name to avoid having to manually specify 81 | | one when using the command. 82 | | 83 | */ 84 | 85 | 'name' => env('API_NAME', null), 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Conditional Requests 90 | |-------------------------------------------------------------------------- 91 | | 92 | | Globally enable conditional requests so that an ETag header is added to 93 | | any successful response. Subsequent requests will perform a check and 94 | | will return a 304 Not Modified. This can also be enabled or disabled 95 | | on certain groups or routes. 96 | | 97 | */ 98 | 99 | 'conditionalRequest' => env('API_CONDITIONAL_REQUEST', true), 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Strict Mode 104 | |-------------------------------------------------------------------------- 105 | | 106 | | Enabling strict mode will require clients to send a valid Accept header 107 | | with every request. This also voids the default API version, meaning 108 | | your API will not be browsable via a web browser. 109 | | 110 | */ 111 | 112 | 'strict' => env('API_STRICT', false), 113 | 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Debug Mode 117 | |-------------------------------------------------------------------------- 118 | | 119 | | Enabling debug mode will result in error responses caused by thrown 120 | | exceptions to have a "debug" key that will be populated with 121 | | more detailed information on the exception. 122 | | 123 | */ 124 | 125 | 'debug' => env('API_DEBUG', false), 126 | 127 | /* 128 | |-------------------------------------------------------------------------- 129 | | Generic Error Format 130 | |-------------------------------------------------------------------------- 131 | | 132 | | When some HTTP exceptions are not caught and dealt with the API will 133 | | generate a generic error response in the format provided. Any 134 | | keys that aren't replaced with corresponding values will be 135 | | removed from the final response. 136 | | 137 | */ 138 | 139 | 'errorFormat' => [ 140 | 'message' => ':message', 141 | 'errors' => ':errors', 142 | 'code' => ':code', 143 | 'status_code' => ':status_code', 144 | 'debug' => ':debug', 145 | ], 146 | 147 | /* 148 | |-------------------------------------------------------------------------- 149 | | API Middleware 150 | |-------------------------------------------------------------------------- 151 | | 152 | | Middleware that will be applied globally to all API requests. 153 | | 154 | */ 155 | 156 | 'middleware' => [ 157 | 158 | ], 159 | 160 | /* 161 | |-------------------------------------------------------------------------- 162 | | Authentication Providers 163 | |-------------------------------------------------------------------------- 164 | | 165 | | The authentication providers that should be used when attempting to 166 | | authenticate an incoming API request. 167 | | 168 | */ 169 | 170 | 'auth' => [ 171 | 172 | ], 173 | 174 | /* 175 | |-------------------------------------------------------------------------- 176 | | Throttling / Rate Limiting 177 | |-------------------------------------------------------------------------- 178 | | 179 | | Consumers of your API can be limited to the amount of requests they can 180 | | make. You can create your own throttles or simply change the default 181 | | throttles. 182 | | 183 | */ 184 | 185 | 'throttling' => [ 186 | 187 | ], 188 | 189 | /* 190 | |-------------------------------------------------------------------------- 191 | | Response Transformer 192 | |-------------------------------------------------------------------------- 193 | | 194 | | Responses can be transformed so that they are easier to format. By 195 | | default a Fractal transformer will be used to transform any 196 | | responses prior to formatting. You can easily replace 197 | | this with your own transformer. 198 | | 199 | */ 200 | 201 | 'transformer' => env('API_TRANSFORMER', Dingo\Api\Transformer\Adapter\Fractal::class), 202 | 203 | /* 204 | |-------------------------------------------------------------------------- 205 | | Response Formats 206 | |-------------------------------------------------------------------------- 207 | | 208 | | Responses can be returned in multiple formats by registering different 209 | | response formatters. You can also customize an existing response 210 | | formatter with a number of options to configure its output. 211 | | 212 | */ 213 | 214 | 'defaultFormat' => env('API_DEFAULT_FORMAT', 'json'), 215 | 216 | 'formats' => [ 217 | 218 | 'json' => Dingo\Api\Http\Response\Format\Json::class, 219 | 220 | ], 221 | 222 | 'formatsOptions' => [ 223 | 224 | 'json' => [ 225 | 'pretty_print' => env('API_JSON_FORMAT_PRETTY_PRINT_ENABLED', false), 226 | 'indent_style' => env('API_JSON_FORMAT_INDENT_STYLE', 'space'), 227 | 'indent_size' => env('API_JSON_FORMAT_INDENT_SIZE', 2), 228 | ], 229 | 230 | ], 231 | 232 | ]; 233 | -------------------------------------------------------------------------------- /src/Transformer/Adapter/Fractal.php: -------------------------------------------------------------------------------- 1 | fractal = $fractal; 61 | $this->includeKey = $includeKey; 62 | $this->includeSeparator = $includeSeparator; 63 | $this->eagerLoading = $eagerLoading; 64 | } 65 | 66 | /** 67 | * Transform a response with a transformer. 68 | * 69 | * @param mixed $response 70 | * @param PHPOpenSourceSaver\Fractal\TransformerAbstract|object $transformer 71 | * @param \Dingo\Api\Transformer\Binding $binding 72 | * @param \Dingo\Api\Http\Request $request 73 | * @return array 74 | */ 75 | public function transform($response, $transformer, Binding $binding, Request $request) 76 | { 77 | $this->parseFractalIncludes($request); 78 | 79 | $resource = $this->createResource($response, $transformer, $parameters = $binding->getParameters()); 80 | 81 | // If the response is a paginator then we'll create a new paginator 82 | // adapter for Laravel and set the paginator instance on our 83 | // collection resource. 84 | if ($response instanceof IlluminatePaginator) { 85 | $paginator = $this->createPaginatorAdapter($response); 86 | 87 | $resource->setPaginator($paginator); 88 | } 89 | 90 | if ($this->shouldEagerLoad($response)) { 91 | $eagerLoads = $this->mergeEagerLoads($transformer, $this->fractal->getRequestedIncludes()); 92 | 93 | if ($transformer instanceof TransformerAbstract) { 94 | // Only eager load the items in available includes 95 | $eagerLoads = array_intersect($eagerLoads, $transformer->getAvailableIncludes()); 96 | } 97 | 98 | $response->load($eagerLoads); 99 | } 100 | 101 | foreach ($binding->getMeta() as $key => $value) { 102 | $resource->setMetaValue($key, $value); 103 | } 104 | 105 | $binding->fireCallback($resource, $this->fractal); 106 | 107 | $identifier = isset($parameters['identifier']) ? $parameters['identifier'] : null; 108 | 109 | return $this->fractal->createData($resource, $identifier)->toArray(); 110 | } 111 | 112 | /** 113 | * Eager loading is only performed when the response is or contains an 114 | * Eloquent collection and eager loading is enabled. 115 | * 116 | * @param mixed $response 117 | * @return bool 118 | */ 119 | protected function shouldEagerLoad($response) 120 | { 121 | if ($response instanceof IlluminatePaginator) { 122 | $response = $response->getCollection(); 123 | } 124 | 125 | return $response instanceof EloquentCollection && $this->eagerLoading; 126 | } 127 | 128 | /** 129 | * Create the Fractal paginator adapter. 130 | * 131 | * @param \Illuminate\Contracts\Pagination\Paginator $paginator 132 | * @return IlluminatePaginatorAdapter|IlluminateSimplePaginatorAdapter 133 | */ 134 | protected function createPaginatorAdapter(IlluminatePaginator $paginator) 135 | { 136 | if ($paginator instanceof IlluminateLengthAwarePaginator) { 137 | return new IlluminatePaginatorAdapter($paginator); 138 | } else { 139 | return new IlluminateSimplePaginatorAdapter($paginator); 140 | } 141 | } 142 | 143 | /** 144 | * Create a Fractal resource instance. 145 | * 146 | * @param mixed $response 147 | * @param \PHPOpenSourceSaver\Fractal\TransformerAbstract $transformer 148 | * @param array $parameters 149 | * @return \PHPOpenSourceSaver\Fractal\Resource\Item|\PHPOpenSourceSaver\Fractal\Resource\Collection 150 | */ 151 | protected function createResource($response, $transformer, array $parameters) 152 | { 153 | $key = isset($parameters['key']) ? $parameters['key'] : null; 154 | 155 | if ($response instanceof IlluminatePaginator || $response instanceof IlluminateCollection) { 156 | return new FractalCollection($response, $transformer, $key); 157 | } 158 | 159 | return new FractalItem($response, $transformer, $key); 160 | } 161 | 162 | /** 163 | * Parse the includes. 164 | * 165 | * @param \Dingo\Api\Http\Request $request 166 | * @return void 167 | */ 168 | public function parseFractalIncludes(Request $request) 169 | { 170 | $includes = $request->input($this->includeKey, ''); 171 | 172 | if (! is_array($includes)) { 173 | $includes = array_map('trim', array_filter(explode($this->includeSeparator, $includes))); 174 | } 175 | 176 | $this->fractal->parseIncludes($includes); 177 | } 178 | 179 | /** 180 | * Get the underlying Fractal instance. 181 | * 182 | * @return \PHPOpenSourceSaver\Fractal\Manager 183 | */ 184 | public function getFractal() 185 | { 186 | return $this->fractal; 187 | } 188 | 189 | /** 190 | * Get includes as their array keys for eager loading. 191 | * 192 | * @param \PHPOpenSourceSaver\Fractal\TransformerAbstract $transformer 193 | * @param string|array $requestedIncludes 194 | * @return array 195 | */ 196 | protected function mergeEagerLoads($transformer, $requestedIncludes) 197 | { 198 | $includes = array_merge($requestedIncludes, $transformer->getDefaultIncludes()); 199 | 200 | $eagerLoads = []; 201 | 202 | foreach ($includes as $key => $value) { 203 | $eagerLoads[] = is_string($key) ? $key : $value; 204 | } 205 | 206 | if (property_exists($transformer, 'lazyLoadedIncludes')) { 207 | $eagerLoads = array_diff($eagerLoads, $transformer->lazyLoadedIncludes); 208 | } 209 | 210 | return $eagerLoads; 211 | } 212 | 213 | /** 214 | * Disable eager loading. 215 | * 216 | * @return \Dingo\Api\Transformer\Adapter\Fractal 217 | */ 218 | public function disableEagerLoading() 219 | { 220 | $this->eagerLoading = false; 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * Enable eager loading. 227 | * 228 | * @return \Dingo\Api\Transformer\Adapter\Fractal 229 | */ 230 | public function enableEagerLoading() 231 | { 232 | $this->eagerLoading = true; 233 | 234 | return $this; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Console/Command/Routes.php: -------------------------------------------------------------------------------- 1 | router = $router; 62 | } 63 | 64 | /** 65 | * Execute the console command. 66 | * 67 | * @return void 68 | */ 69 | public function fire() 70 | { 71 | $this->routes = $this->router->getRoutes(); 72 | 73 | parent::fire(); 74 | } 75 | 76 | /** 77 | * Execute the console command. 78 | * 79 | * @return void 80 | */ 81 | public function handle() 82 | { 83 | $this->routes = $this->router->getRoutes(); 84 | 85 | parent::handle(); 86 | } 87 | 88 | /** 89 | * Compile the routes into a displayable format. 90 | * 91 | * @return array 92 | */ 93 | protected function getRoutes() 94 | { 95 | $routes = []; 96 | 97 | foreach ($this->router->getRoutes() as $collection) { 98 | foreach ($collection->getRoutes() as $route) { 99 | $routes[] = $this->filterRoute([ 100 | 'host' => $route->domain(), 101 | 'domain' => $route->domain(), 102 | 'middleware' => json_encode($route->middleware()), 103 | 'method' => implode('|', $route->methods()), 104 | 'uri' => $route->uri(), 105 | 'name' => $route->getName(), 106 | 'action' => $route->getActionName(), 107 | 'protected' => $route->isProtected() ? 'Yes' : 'No', 108 | 'versions' => implode(', ', $route->versions()), 109 | 'scopes' => implode(', ', $route->scopes()), 110 | 'rate' => $this->routeRateLimit($route), 111 | ]); 112 | } 113 | } 114 | 115 | if ($sort = $this->option('sort')) { 116 | $routes = Arr::sort($routes, function ($value) use ($sort) { 117 | return $value[$sort]; 118 | }); 119 | } 120 | 121 | if ($this->option('reverse')) { 122 | $routes = array_reverse($routes); 123 | } 124 | 125 | if ($this->option('short')) { 126 | $this->headers = ['Method', 'URI', 'Name', 'Version(s)']; 127 | 128 | $routes = array_map(function ($item) { 129 | return Arr::only($item, ['method', 'uri', 'name', 'versions']); 130 | }, $routes); 131 | } 132 | 133 | return array_filter(array_unique($routes, SORT_REGULAR)); 134 | } 135 | 136 | /** 137 | * Display the routes rate limiting requests per second. This takes the limit 138 | * and divides it by the expiration time in seconds to give you a rough 139 | * idea of how many requests you'd be able to fire off per second 140 | * on the route. 141 | * 142 | * @param \Dingo\Api\Routing\Route $route 143 | * @return null|string 144 | */ 145 | protected function routeRateLimit($route) 146 | { 147 | [$limit, $expires] = [$route->getRateLimit(), $route->getRateLimitExpiration()]; 148 | 149 | if ($limit && $expires) { 150 | return sprintf('%s req/s', round($limit / ($expires * 60), 2)); 151 | } 152 | } 153 | 154 | /** 155 | * Filter the route by URI, Version, Scopes and / or name. 156 | * 157 | * @param array $route 158 | * @return array|null 159 | */ 160 | protected function filterRoute(array $route) 161 | { 162 | $filters = ['name', 'path', 'protected', 'unprotected', 'versions', 'scopes']; 163 | 164 | foreach ($filters as $filter) { 165 | if ($this->option($filter) && ! $this->{'filterBy'.ucfirst($filter)}($route)) { 166 | return; 167 | } 168 | } 169 | 170 | return $route; 171 | } 172 | 173 | /** 174 | * Get the console command options. 175 | * 176 | * @return array 177 | */ 178 | protected function getOptions() 179 | { 180 | $options = parent::getOptions(); 181 | 182 | foreach ($options as $key => $option) { 183 | if ($option[0] == 'sort') { 184 | unset($options[$key]); 185 | } 186 | } 187 | 188 | return array_merge( 189 | $options, 190 | [ 191 | ['sort', null, InputOption::VALUE_OPTIONAL, 'The column (domain, method, uri, name, action) to sort by'], 192 | ['versions', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Filter the routes by version'], 193 | ['scopes', 'S', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Filter the routes by scopes'], 194 | ['protected', null, InputOption::VALUE_NONE, 'Filter the protected routes'], 195 | ['unprotected', null, InputOption::VALUE_NONE, 'Filter the unprotected routes'], 196 | ['short', null, InputOption::VALUE_NONE, 'Get an abridged version of the routes'], 197 | ] 198 | ); 199 | } 200 | 201 | /** 202 | * Filter the route by its path. 203 | * 204 | * @param array $route 205 | * @return bool 206 | */ 207 | protected function filterByPath(array $route) 208 | { 209 | return Str::contains($route['uri'], $this->option('path')); 210 | } 211 | 212 | /** 213 | * Filter the route by whether or not it is protected. 214 | * 215 | * @param array $route 216 | * @return bool 217 | */ 218 | protected function filterByProtected(array $route) 219 | { 220 | return $this->option('protected') && $route['protected'] == 'Yes'; 221 | } 222 | 223 | /** 224 | * Filter the route by whether or not it is unprotected. 225 | * 226 | * @param array $route 227 | * @return bool 228 | */ 229 | protected function filterByUnprotected(array $route) 230 | { 231 | return $this->option('unprotected') && $route['protected'] == 'No'; 232 | } 233 | 234 | /** 235 | * Filter the route by its versions. 236 | * 237 | * @param array $route 238 | * @return bool 239 | */ 240 | protected function filterByVersions(array $route) 241 | { 242 | foreach ($this->option('versions') as $version) { 243 | if (Str::contains($route['versions'], $version)) { 244 | return true; 245 | } 246 | } 247 | 248 | return false; 249 | } 250 | 251 | /** 252 | * Filter the route by its name. 253 | * 254 | * @param array $route 255 | * @return bool 256 | */ 257 | protected function filterByName(array $route) 258 | { 259 | return Str::contains($route['name'], $this->option('name')); 260 | } 261 | 262 | /** 263 | * Filter the route by its scopes. 264 | * 265 | * @param array $route 266 | * @return bool 267 | */ 268 | protected function filterByScopes(array $route) 269 | { 270 | foreach ($this->option('scopes') as $scope) { 271 | if (Str::contains($route['scopes'], $scope)) { 272 | return true; 273 | } 274 | } 275 | 276 | return false; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/Http/RateLimit/Handler.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 76 | $this->container = $container; 77 | $this->throttles = new Collection($throttles); 78 | } 79 | 80 | /** 81 | * Execute the rate limiting for the given request. 82 | * 83 | * @param \Dingo\Api\Http\Request $request 84 | * @param int $limit 85 | * @param int $expires 86 | * @return void 87 | */ 88 | public function rateLimitRequest(Request $request, $limit = 0, $expires = 0) 89 | { 90 | $this->request = $request; 91 | 92 | // If the throttle instance is already set then we'll just carry on as 93 | // per usual. 94 | if ($this->throttle instanceof Throttle) { 95 | 96 | // If the developer specified a certain amount of requests or expiration 97 | // time on a specific route then we'll always use the route specific 98 | // throttle with the given values. 99 | } elseif ($limit > 0 || $expires > 0) { 100 | $this->throttle = new Route(['limit' => $limit, 'expires' => $expires]); 101 | $this->keyPrefix = sha1($request->path()); 102 | 103 | // Otherwise we'll use the throttle that gives the consumer the largest 104 | // amount of requests. If no matching throttle is found then rate 105 | // limiting will not be imposed for the request. 106 | } else { 107 | $this->throttle = $this->getMatchingThrottles()->sort(function ($a, $b) { 108 | return $a->getLimit() <=> $b->getLimit(); 109 | })->last(); 110 | } 111 | 112 | if (is_null($this->throttle)) { 113 | return; 114 | } 115 | 116 | if ($this->throttle instanceof HasRateLimiter) { 117 | $this->setRateLimiter([$this->throttle, 'getRateLimiter']); 118 | } 119 | 120 | $this->prepareCacheStore(); 121 | 122 | $this->cache('requests', 0, $this->throttle->getExpires()); 123 | $this->cache('expires', $this->throttle->getExpires(), $this->throttle->getExpires()); 124 | $this->cache('reset', time() + ($this->throttle->getExpires() * 60), $this->throttle->getExpires()); 125 | $this->increment('requests'); 126 | } 127 | 128 | /** 129 | * Prepare the cache store. 130 | * 131 | * @return void 132 | */ 133 | protected function prepareCacheStore() 134 | { 135 | if ($this->retrieve('expires') != $this->throttle->getExpires()) { 136 | $this->forget('requests'); 137 | $this->forget('expires'); 138 | $this->forget('reset'); 139 | } 140 | } 141 | 142 | /** 143 | * Determine if the rate limit has been exceeded. 144 | * 145 | * @return bool 146 | */ 147 | public function exceededRateLimit() 148 | { 149 | return $this->requestWasRateLimited() ? $this->retrieve('requests') > $this->throttle->getLimit() : false; 150 | } 151 | 152 | /** 153 | * Get matching throttles after executing the condition of each throttle. 154 | * 155 | * @return \Illuminate\Support\Collection 156 | */ 157 | protected function getMatchingThrottles() 158 | { 159 | return $this->throttles->filter(function ($throttle) { 160 | return $throttle->match($this->container); 161 | }); 162 | } 163 | 164 | /** 165 | * Namespace a cache key. 166 | * 167 | * @param string $key 168 | * @return string 169 | */ 170 | protected function key($key) 171 | { 172 | return sprintf('dingo.api.%s.%s', $key, $this->getRateLimiter()); 173 | } 174 | 175 | /** 176 | * Cache a value under a given key for a certain amount of minutes. 177 | * 178 | * @param string $key 179 | * @param mixed $value 180 | * @param int $minutes 181 | * @return void 182 | */ 183 | protected function cache($key, $value, $minutes) 184 | { 185 | $this->cache->add($this->key($key), $value, Carbon::now()->addMinutes($minutes)); 186 | } 187 | 188 | /** 189 | * Retrieve a value from the cache store. 190 | * 191 | * @param string $key 192 | * @return mixed 193 | */ 194 | protected function retrieve($key) 195 | { 196 | return $this->cache->get($this->key($key)); 197 | } 198 | 199 | /** 200 | * Increment a key in the cache. 201 | * 202 | * @param string $key 203 | * @return void 204 | */ 205 | protected function increment($key) 206 | { 207 | $this->cache->increment($this->key($key)); 208 | } 209 | 210 | /** 211 | * Forget a key in the cache. 212 | * 213 | * @param string $key 214 | * @return void 215 | */ 216 | protected function forget($key) 217 | { 218 | $this->cache->forget($this->key($key)); 219 | } 220 | 221 | /** 222 | * Determine if the request was rate limited. 223 | * 224 | * @return bool 225 | */ 226 | public function requestWasRateLimited() 227 | { 228 | return ! is_null($this->throttle); 229 | } 230 | 231 | /** 232 | * Get the rate limiter. 233 | * 234 | * @return string 235 | */ 236 | public function getRateLimiter() 237 | { 238 | return call_user_func($this->limiter ?: function ($container, $request) { 239 | return $request->getClientIp(); 240 | }, $this->container, $this->request); 241 | } 242 | 243 | /** 244 | * Set the rate limiter. 245 | * 246 | * @param callable $limiter 247 | * @return void 248 | */ 249 | public function setRateLimiter(callable $limiter) 250 | { 251 | $this->limiter = $limiter; 252 | } 253 | 254 | /** 255 | * Set the throttle to use for rate limiting. 256 | * 257 | * @param string|\Dingo\Api\Contract\Http\RateLimit\Throttle $throttle 258 | * @return void 259 | */ 260 | public function setThrottle($throttle) 261 | { 262 | if (is_string($throttle)) { 263 | $throttle = $this->container->make($throttle); 264 | } 265 | 266 | $this->throttle = $throttle; 267 | } 268 | 269 | /** 270 | * Get the throttle used to rate limit the request. 271 | * 272 | * @return \Dingo\Api\Contract\Http\RateLimit\Throttle 273 | */ 274 | public function getThrottle() 275 | { 276 | return $this->throttle; 277 | } 278 | 279 | /** 280 | * Get the limit of the throttle used. 281 | * 282 | * @return int 283 | */ 284 | public function getThrottleLimit() 285 | { 286 | return $this->throttle->getLimit(); 287 | } 288 | 289 | /** 290 | * Get the remaining limit before the consumer is rate limited. 291 | * 292 | * @return int 293 | */ 294 | public function getRemainingLimit() 295 | { 296 | $remaining = $this->throttle->getLimit() - $this->retrieve('requests'); 297 | 298 | return $remaining > 0 ? $remaining : 0; 299 | } 300 | 301 | /** 302 | * Get the timestamp for when the current rate limiting will expire. 303 | * 304 | * @return int 305 | */ 306 | public function getRateLimitReset() 307 | { 308 | return $this->retrieve('reset'); 309 | } 310 | 311 | /** 312 | * Extend the rate limiter by adding a new throttle. 313 | * 314 | * @param callable|\Dingo\Api\Http\RateLimit\Throttle $throttle 315 | * @return void 316 | */ 317 | public function extend($throttle) 318 | { 319 | if (is_callable($throttle)) { 320 | $throttle = call_user_func($throttle, $this->container); 321 | } 322 | 323 | $this->throttles->push($throttle); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/Http/Response/Factory.php: -------------------------------------------------------------------------------- 1 | transformer = $transformer; 32 | } 33 | 34 | /** 35 | * Respond with a created response and associate a location if provided. 36 | * 37 | * @param null|string $location 38 | * @return \Dingo\Api\Http\Response 39 | */ 40 | public function created($location = null, $content = null) 41 | { 42 | $response = new Response($content); 43 | $response->setStatusCode(201); 44 | 45 | if (! is_null($location)) { 46 | $response->header('Location', $location); 47 | } 48 | 49 | return $response; 50 | } 51 | 52 | /** 53 | * Respond with an accepted response and associate a location and/or content if provided. 54 | * 55 | * @param null|string $location 56 | * @param mixed $content 57 | * @return \Dingo\Api\Http\Response 58 | */ 59 | public function accepted($location = null, $content = null) 60 | { 61 | $response = new Response($content); 62 | $response->setStatusCode(202); 63 | 64 | if (! is_null($location)) { 65 | $response->header('Location', $location); 66 | } 67 | 68 | return $response; 69 | } 70 | 71 | /** 72 | * Respond with a no content response. 73 | * 74 | * @return \Dingo\Api\Http\Response 75 | */ 76 | public function noContent() 77 | { 78 | $response = new Response(null); 79 | 80 | return $response->setStatusCode(204); 81 | } 82 | 83 | /** 84 | * Bind a collection to a transformer and start building a response. 85 | * 86 | * @param \Illuminate\Support\Collection $collection 87 | * @param string|callable|object $transformer 88 | * @param array|\Closure $parameters 89 | * @param \Closure|null $after 90 | * @return \Dingo\Api\Http\Response 91 | */ 92 | public function collection(Collection $collection, $transformer = null, $parameters = [], ?Closure $after = null) 93 | { 94 | if ($collection->isEmpty()) { 95 | $class = get_class($collection); 96 | } else { 97 | $class = get_class($collection->first()); 98 | } 99 | 100 | if ($parameters instanceof \Closure) { 101 | $after = $parameters; 102 | $parameters = []; 103 | } 104 | 105 | if ($transformer !== null) { 106 | $binding = $this->transformer->register($class, $transformer, $parameters, $after); 107 | } else { 108 | $binding = $this->transformer->getBinding($collection); 109 | } 110 | 111 | return new Response($collection, 200, [], $binding); 112 | } 113 | 114 | /** 115 | * Bind an item to a transformer and start building a response. 116 | * 117 | * @param object $item 118 | * @param null|string|callable|object $transformer 119 | * @param array $parameters 120 | * @param \Closure|null $after 121 | * @return \Dingo\Api\Http\Response 122 | */ 123 | public function item($item, $transformer = null, $parameters = [], ?Closure $after = null) 124 | { 125 | // Check for $item being null 126 | if (! is_null($item)) { 127 | $class = get_class($item); 128 | } else { 129 | $class = \stdClass::class; 130 | } 131 | 132 | if ($parameters instanceof \Closure) { 133 | $after = $parameters; 134 | $parameters = []; 135 | } 136 | 137 | if ($transformer !== null) { 138 | $binding = $this->transformer->register($class, $transformer, $parameters, $after); 139 | } else { 140 | $binding = $this->transformer->getBinding($item); 141 | } 142 | 143 | return new Response($item, 200, [], $binding); 144 | } 145 | 146 | /** 147 | * Bind an arbitrary array to a transformer and start building a response. 148 | * 149 | * @param array $array 150 | * @param $transformer 151 | * @param array $parameters 152 | * @param Closure|null $after 153 | * @return Response 154 | */ 155 | public function array(array $array, $transformer = null, $parameters = [], ?Closure $after = null) 156 | { 157 | if ($parameters instanceof \Closure) { 158 | $after = $parameters; 159 | $parameters = []; 160 | } 161 | 162 | // For backwards compatibility, allow no transformer 163 | if ($transformer) { 164 | // Use the PHP stdClass for this purpose, as a work-around, since we need to register a class binding 165 | $class = 'stdClass'; 166 | // This will convert the array into an stdClass 167 | $array = (object) $array; 168 | 169 | $binding = $this->transformer->register($class, $transformer, $parameters, $after); 170 | } else { 171 | $binding = null; 172 | } 173 | 174 | return new Response($array, 200, [], $binding); 175 | } 176 | 177 | /** 178 | * Bind a paginator to a transformer and start building a response. 179 | * 180 | * @param \Illuminate\Contracts\Pagination\Paginator $paginator 181 | * @param null|string|callable|object $transformer 182 | * @param array $parameters 183 | * @param \Closure|null $after 184 | * @return \Dingo\Api\Http\Response 185 | */ 186 | public function paginator(Paginator $paginator, $transformer = null, array $parameters = [], ?Closure $after = null) 187 | { 188 | if ($paginator->isEmpty()) { 189 | $class = get_class($paginator); 190 | } else { 191 | $class = get_class($paginator->first()); 192 | } 193 | 194 | if ($transformer !== null) { 195 | $binding = $this->transformer->register($class, $transformer, $parameters, $after); 196 | } else { 197 | $binding = $this->transformer->getBinding($paginator->first()); 198 | } 199 | 200 | return new Response($paginator, 200, [], $binding); 201 | } 202 | 203 | /** 204 | * Return an error response. 205 | * 206 | * @param string $message 207 | * @param int $statusCode 208 | * @return void 209 | * 210 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 211 | */ 212 | public function error($message, $statusCode) 213 | { 214 | throw new HttpException($statusCode, $message); 215 | } 216 | 217 | /** 218 | * Return a 404 not found error. 219 | * 220 | * @param string $message 221 | * @return void 222 | * 223 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 224 | */ 225 | public function errorNotFound($message = 'Not Found') 226 | { 227 | $this->error($message, 404); 228 | } 229 | 230 | /** 231 | * Return a 400 bad request error. 232 | * 233 | * @param string $message 234 | * @return void 235 | * 236 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 237 | */ 238 | public function errorBadRequest($message = 'Bad Request') 239 | { 240 | $this->error($message, 400); 241 | } 242 | 243 | /** 244 | * Return a 403 forbidden error. 245 | * 246 | * @param string $message 247 | * @return void 248 | * 249 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 250 | */ 251 | public function errorForbidden($message = 'Forbidden') 252 | { 253 | $this->error($message, 403); 254 | } 255 | 256 | /** 257 | * Return a 500 internal server error. 258 | * 259 | * @param string $message 260 | * @return void 261 | * 262 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 263 | */ 264 | public function errorInternal($message = 'Internal Error') 265 | { 266 | $this->error($message, 500); 267 | } 268 | 269 | /** 270 | * Return a 401 unauthorized error. 271 | * 272 | * @param string $message 273 | * @return void 274 | * 275 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 276 | */ 277 | public function errorUnauthorized($message = 'Unauthorized') 278 | { 279 | $this->error($message, 401); 280 | } 281 | 282 | /** 283 | * Return a 405 method not allowed error. 284 | * 285 | * @param string $message 286 | * @return void 287 | * 288 | * @throws \Symfony\Component\HttpKernel\Exception\HttpException 289 | */ 290 | public function errorMethodNotAllowed($message = 'Method Not Allowed') 291 | { 292 | $this->error($message, 405); 293 | } 294 | 295 | /** 296 | * Call magic methods beginning with "with". 297 | * 298 | * @param string $method 299 | * @param array $parameters 300 | * @return mixed 301 | * 302 | * @throws \ErrorException 303 | */ 304 | public function __call($method, $parameters) 305 | { 306 | if (Str::startsWith($method, 'with')) { 307 | return call_user_func_array([$this, Str::camel(substr($method, 4))], $parameters); 308 | 309 | // Because PHP won't let us name the method "array" we'll simply watch for it 310 | // in here and return the new binding. Gross. This is now DEPRECATED and 311 | // should not be used. Just return an array or a new response instance. 312 | } elseif ($method == 'array') { 313 | return new Response($parameters[0]); 314 | } 315 | 316 | throw new ErrorException('Undefined method '.get_class($this).'::'.$method); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/Exception/Handler.php: -------------------------------------------------------------------------------- 1 | parentHandler = $parentHandler; 71 | $this->format = $format; 72 | $this->debug = $debug; 73 | } 74 | 75 | /** 76 | * Report or log an exception. 77 | * 78 | * @param Throwable $exception 79 | * @return void 80 | */ 81 | public function report(Throwable $throwable) 82 | { 83 | $this->parentHandler->report($throwable); 84 | } 85 | 86 | /** 87 | * Determine if the exception should be reported. 88 | * 89 | * @param Throwable $e 90 | * @return bool 91 | */ 92 | public function shouldReport(Throwable $e) 93 | { 94 | return $this->parentHandler->shouldReport($e); 95 | } 96 | 97 | /** 98 | * Render an exception into an HTTP response. 99 | * 100 | * @param Request $request 101 | * @param Throwable $exception 102 | * @return mixed 103 | * 104 | * @throws Exception 105 | */ 106 | public function render($request, Throwable $exception) 107 | { 108 | return $this->handle($exception); 109 | } 110 | 111 | /** 112 | * Render an exception to the console. 113 | * 114 | * @param OutputInterface $output 115 | * @param Throwable $exception 116 | * @return mixed 117 | */ 118 | public function renderForConsole($output, Throwable $exception) 119 | { 120 | return $this->parentHandler->renderForConsole($output, $exception); 121 | } 122 | 123 | /** 124 | * Register a new exception handler. 125 | * 126 | * @param callable $callback 127 | * @return void 128 | */ 129 | public function register(callable $callback) 130 | { 131 | $hint = $this->handlerHint($callback); 132 | 133 | $this->handlers[$hint] = $callback; 134 | } 135 | 136 | /** 137 | * Handle an exception if it has an existing handler. 138 | * 139 | * @param Throwable|Exception $exception 140 | * @return Response 141 | */ 142 | public function handle($exception) 143 | { 144 | // Convert Eloquent's 500 ModelNotFoundException into a 404 NotFoundHttpException 145 | if ($exception instanceof ModelNotFoundException) { 146 | $exception = new NotFoundHttpException($exception->getMessage(), $exception); 147 | } 148 | 149 | foreach ($this->handlers as $hint => $handler) { 150 | if (! $exception instanceof $hint) { 151 | continue; 152 | } 153 | 154 | if ($response = $handler($exception)) { 155 | if (! $response instanceof BaseResponse) { 156 | $response = new Response($response, $this->getExceptionStatusCode($exception)); 157 | } 158 | 159 | return $response->withException($exception); 160 | } 161 | } 162 | 163 | return $this->genericResponse($exception)->withException($exception); 164 | } 165 | 166 | /** 167 | * Handle a generic error response if there is no handler available. 168 | * 169 | * @param Throwable $exception 170 | * @return Response 171 | * 172 | * @throws Throwable 173 | */ 174 | protected function genericResponse(Throwable $exception) 175 | { 176 | $replacements = $this->prepareReplacements($exception); 177 | 178 | $response = $this->newResponseArray(); 179 | 180 | array_walk_recursive($response, function (&$value, $key) use ($replacements) { 181 | if (Str::startsWith($value, ':') && isset($replacements[$value])) { 182 | $value = $replacements[$value]; 183 | } 184 | }); 185 | 186 | $response = $this->recursivelyRemoveEmptyReplacements($response); 187 | 188 | return new Response($response, $this->getStatusCode($exception), $this->getHeaders($exception)); 189 | } 190 | 191 | /** 192 | * Get the status code from the exception. 193 | * 194 | * @param Throwable $exception 195 | * @return int 196 | */ 197 | protected function getStatusCode(Throwable $exception) 198 | { 199 | $statusCode = null; 200 | 201 | if ($exception instanceof ValidationException) { 202 | $statusCode = $exception->status; 203 | } elseif ($exception instanceof HttpExceptionInterface) { 204 | $statusCode = $exception->getStatusCode(); 205 | } else { 206 | // By default throw 500 207 | $statusCode = 500; 208 | } 209 | 210 | // Be extra defensive 211 | if ($statusCode < 100 || $statusCode > 599) { 212 | $statusCode = 500; 213 | } 214 | 215 | return $statusCode; 216 | } 217 | 218 | /** 219 | * Get the headers from the exception. 220 | * 221 | * @param Throwable $exception 222 | * @return array 223 | */ 224 | protected function getHeaders(Throwable $exception) 225 | { 226 | return $exception instanceof HttpExceptionInterface ? $exception->getHeaders() : []; 227 | } 228 | 229 | /** 230 | * Prepare the replacements array by gathering the keys and values. 231 | * 232 | * @param Throwable $exception 233 | * @return array 234 | */ 235 | protected function prepareReplacements(Throwable $exception) 236 | { 237 | $statusCode = $this->getStatusCode($exception); 238 | 239 | if (! $message = $exception->getMessage()) { 240 | $message = sprintf('%d %s', $statusCode, Response::$statusTexts[$statusCode]); 241 | } 242 | 243 | $replacements = [ 244 | ':message' => $message, 245 | ':status_code' => $statusCode, 246 | ]; 247 | 248 | if ($exception instanceof MessageBagErrors && $exception->hasErrors()) { 249 | $replacements[':errors'] = $exception->getErrors(); 250 | } 251 | 252 | if ($exception instanceof ValidationException) { 253 | $replacements[':errors'] = $exception->errors(); 254 | $replacements[':status_code'] = $exception->status; 255 | } 256 | 257 | if ($code = $exception->getCode()) { 258 | $replacements[':code'] = $code; 259 | } 260 | 261 | if ($this->runningInDebugMode()) { 262 | $replacements[':debug'] = [ 263 | 'line' => $exception->getLine(), 264 | 'file' => $exception->getFile(), 265 | 'class' => get_class($exception), 266 | 'trace' => explode("\n", $exception->getTraceAsString()), 267 | ]; 268 | 269 | // Attach trace of previous exception, if exists 270 | if (! is_null($exception->getPrevious())) { 271 | $currentTrace = $replacements[':debug']['trace']; 272 | 273 | $replacements[':debug']['trace'] = [ 274 | 'previous' => explode("\n", $exception->getPrevious()->getTraceAsString()), 275 | 'current' => $currentTrace, 276 | ]; 277 | } 278 | } 279 | 280 | return array_merge($replacements, $this->replacements); 281 | } 282 | 283 | /** 284 | * Set user defined replacements. 285 | * 286 | * @param array $replacements 287 | * @return void 288 | */ 289 | public function setReplacements(array $replacements) 290 | { 291 | $this->replacements = $replacements; 292 | } 293 | 294 | /** 295 | * Recursively remove any empty replacement values in the response array. 296 | * 297 | * @param array $input 298 | * @return array 299 | */ 300 | protected function recursivelyRemoveEmptyReplacements(array $input) 301 | { 302 | foreach ($input as &$value) { 303 | if (is_array($value)) { 304 | $value = $this->recursivelyRemoveEmptyReplacements($value); 305 | } 306 | } 307 | 308 | return array_filter($input, function ($value) { 309 | if (is_string($value)) { 310 | return ! Str::startsWith($value, ':'); 311 | } 312 | 313 | return true; 314 | }); 315 | } 316 | 317 | /** 318 | * Create a new response array with replacement values. 319 | * 320 | * @return array 321 | */ 322 | protected function newResponseArray() 323 | { 324 | return $this->format; 325 | } 326 | 327 | /** 328 | * Get the exception status code. 329 | * 330 | * @param Exception $exception 331 | * @param int $defaultStatusCode 332 | * @return int 333 | */ 334 | protected function getExceptionStatusCode(Exception $exception, $defaultStatusCode = 500) 335 | { 336 | return ($exception instanceof HttpExceptionInterface) ? $exception->getStatusCode() : $defaultStatusCode; 337 | } 338 | 339 | /** 340 | * Determines if we are running in debug mode. 341 | * 342 | * @return bool 343 | */ 344 | protected function runningInDebugMode() 345 | { 346 | return $this->debug; 347 | } 348 | 349 | /** 350 | * Get the hint for an exception handler. 351 | * 352 | * @param callable $callback 353 | * @return string 354 | */ 355 | protected function handlerHint(callable $callback) 356 | { 357 | $reflection = new ReflectionFunction($callback); 358 | 359 | $exception = $reflection->getParameters()[0]; 360 | $reflectionType = $exception->getType(); 361 | 362 | if ($reflectionType && ! $reflectionType->isBuiltin()) { 363 | if ($reflectionType instanceof \ReflectionNamedType) { 364 | return $reflectionType->getName(); 365 | } 366 | } 367 | 368 | return ''; 369 | } 370 | 371 | /** 372 | * Get the exception handlers. 373 | * 374 | * @return array 375 | */ 376 | public function getHandlers() 377 | { 378 | return $this->handlers; 379 | } 380 | 381 | /** 382 | * Set the error format array. 383 | * 384 | * @param array $format 385 | * @return void 386 | */ 387 | public function setErrorFormat(array $format) 388 | { 389 | $this->format = $format; 390 | } 391 | 392 | /** 393 | * Set the debug mode. 394 | * 395 | * @param bool $debug 396 | * @return void 397 | */ 398 | public function setDebug($debug) 399 | { 400 | $this->debug = $debug; 401 | } 402 | 403 | /** 404 | * Register a reportable callback. 405 | * 406 | * @param callable $reportUsing 407 | * @return \Illuminate\Foundation\Exceptions\ReportableHandler 408 | */ 409 | public function reportable(callable $reportUsing) 410 | { 411 | if (! $reportUsing instanceof Closure) { 412 | $reportUsing = Closure::fromCallable($reportUsing); 413 | } 414 | 415 | return tap(new ReportableHandler($reportUsing), function ($callback) { 416 | $this->reportCallbacks[] = $callback; 417 | }); 418 | } 419 | } 420 | --------------------------------------------------------------------------------