forked from admin/services_core
Ubah dari Swagger ke Dedoc lalu buat API Ticket dan TicketType
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `scramble` will be documented in this file.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) dedoc <litvinenko95@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
<p>
|
||||
<a href="https://scramble.dedoc.co" target="_blank">
|
||||
<img src="./.github/gh-img.png?v=1" alt="Scramble – Laravel API documentation generator"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Scramble
|
||||
|
||||
Scramble generates API documentation for Laravel project. Without requiring you to manually write PHPDoc annotations. Docs are generated in OpenAPI 3.1.0 format.
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find full documentation at [scramble.dedoc.co](https://scramble.dedoc.co).
|
||||
|
||||
## Introduction
|
||||
|
||||
The main motto of the project is generating your API documentation without requiring you to annotate your code.
|
||||
|
||||
This allows you to focus on code and avoid annotating every possible param/field as it may result in outdated documentation. By generating docs automatically from the code your API will always have up-to-date docs which you can trust.
|
||||
|
||||
## Installation
|
||||
You can install the package via composer:
|
||||
```shell
|
||||
composer require dedoc/scramble
|
||||
```
|
||||
|
||||
## Usage
|
||||
After install you will have 2 routes added to your application:
|
||||
|
||||
- `/docs/api` - UI viewer for your documentation
|
||||
- `/docs/api.json` - Open API document in JSON format describing your API.
|
||||
|
||||
By default, these routes are available only in `local` environment. You can change this behavior [by defining `viewApiDocs` gate](https://scramble.dedoc.co/usage/getting-started#docs-authorization).
|
||||
|
||||
---
|
||||
|
||||
<p>
|
||||
<a href="https://savelife.in.ua/en/donate-en/" target="_blank">
|
||||
<img src="./.github/gh-promo.svg?v=1" alt="Donate"/>
|
||||
</a>
|
||||
</p>
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "dedoc/scramble",
|
||||
"description": "Automatic generation of API documentation for Laravel applications.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"documentation",
|
||||
"openapi"
|
||||
],
|
||||
"homepage": "https://github.com/dedoc/scramble",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Roman Lytvynenko",
|
||||
"email": "litvinenko95@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
|
||||
"myclabs/deep-copy": "^1.12",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"phpstan/phpdoc-parser": "^1.0|^2.0",
|
||||
"spatie/laravel-package-tools": "^1.9.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^3.3",
|
||||
"laravel/pint": "^v1.1.0",
|
||||
"nunomaduro/collision": "^7.0|^8.0",
|
||||
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
|
||||
"pestphp/pest": "^2.34|^3.7|^4.4",
|
||||
"pestphp/pest-plugin-laravel": "^2.3|^3.1|^4.1",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^10.5|^11.5.3|^12.5.12",
|
||||
"spatie/laravel-permission": "^6.10|^7.2",
|
||||
"spatie/pest-plugin-snapshots": "^2.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dedoc\\Scramble\\": "src",
|
||||
"Dedoc\\Scramble\\Database\\Factories\\": "database/factories"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Dedoc\\Scramble\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"analyse": "vendor/bin/phpstan analyse",
|
||||
"test": "vendor/bin/pest",
|
||||
"test-coverage": "vendor/bin/pest --coverage",
|
||||
"format": "vendor/bin/pint"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"phpstan/extension-installer": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Dedoc\\Scramble\\ScrambleServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||
|
||||
return [
|
||||
/*
|
||||
* Which routes to document. String or array form; use Scramble::routes() for custom selection.
|
||||
*
|
||||
* 'api_path' => [
|
||||
* 'include' => 'api',
|
||||
* 'exclude' => ['api/internal'],
|
||||
* ],
|
||||
*
|
||||
* Without *, patterns match path segments (api matches api and api/users, not apiary).
|
||||
* With *, Str::is is used (e.g. api/v*).
|
||||
*
|
||||
* One static include → default server is /{include} and paths are stripped (/users).
|
||||
* Multiple includes or wildcards → server defaults to / and paths stay full (/api/users).
|
||||
* Override with `servers`, or use Scramble::registerApi() for separate bases.
|
||||
*/
|
||||
'api_path' => 'api',
|
||||
|
||||
/*
|
||||
* Your API domain. By default, app domain is used. This is also a part of the default API routes
|
||||
* matcher, so when implementing your own, make sure you use this config if needed.
|
||||
*/
|
||||
'api_domain' => null,
|
||||
|
||||
/*
|
||||
* The path where your OpenAPI specification will be exported.
|
||||
*/
|
||||
'export_path' => 'api.json',
|
||||
|
||||
/*
|
||||
* Cache configuration for the generated OpenAPI document.
|
||||
*
|
||||
* Use `scramble:cache` to warm the cache and `scramble:clear` to invalidate it.
|
||||
*/
|
||||
'cache' => [
|
||||
'key' => 'scramble.openapi',
|
||||
'store' => 'file',
|
||||
],
|
||||
|
||||
'info' => [
|
||||
/*
|
||||
* API version.
|
||||
*/
|
||||
'version' => env('API_VERSION', '0.0.1'),
|
||||
|
||||
/*
|
||||
* Description rendered on the home page of the API documentation (`/docs/api`).
|
||||
*/
|
||||
'description' => '',
|
||||
],
|
||||
|
||||
'ui' => [
|
||||
'title' => null,
|
||||
],
|
||||
|
||||
'renderer' => 'elements',
|
||||
|
||||
'renderers' => [
|
||||
/*
|
||||
* Stoplight Elements config options: https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options
|
||||
*/
|
||||
'elements' => [
|
||||
'view' => 'scramble::docs',
|
||||
'theme' => 'light',
|
||||
'hideTryIt' => false,
|
||||
'hideSchemas' => false,
|
||||
'logo' => '',
|
||||
'tryItCredentialsPolicy' => 'include',
|
||||
'layout' => 'responsive',
|
||||
'router' => 'hash',
|
||||
],
|
||||
/*
|
||||
* Scalar API reference config options: https://scalar.com/products/api-references/configuration
|
||||
*/
|
||||
'scalar' => [
|
||||
'view' => 'scramble::scalar',
|
||||
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
|
||||
'theme' => 'laravel',
|
||||
'proxyUrl' => 'https://proxy.scalar.com',
|
||||
'darkMode' => false,
|
||||
'showDeveloperTools' => 'never',
|
||||
'agent' => ['disabled' => true],
|
||||
'credentials' => 'include',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* The list of servers of the API. By default, when `null`, server URL will be created from
|
||||
* `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you
|
||||
* will need to specify the local server URL manually (if needed).
|
||||
*
|
||||
* Example of non-default config (final URLs are generated using Laravel `url` helper):
|
||||
*
|
||||
* ```php
|
||||
* 'servers' => [
|
||||
* 'Live' => 'api',
|
||||
* 'Prod' => 'https://scramble.dedoc.co/api',
|
||||
* ],
|
||||
* ```
|
||||
*/
|
||||
'servers' => null,
|
||||
|
||||
/**
|
||||
* Determines how Scramble stores the descriptions of enum cases.
|
||||
* Available options:
|
||||
* - 'description' – Case descriptions are stored as the enum schema's description using table formatting.
|
||||
* - 'extension' – Case descriptions are stored in the `x-enumDescriptions` enum schema extension.
|
||||
*
|
||||
* @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions
|
||||
* - false - Case descriptions are ignored.
|
||||
*/
|
||||
'enum_cases_description_strategy' => 'description',
|
||||
|
||||
/**
|
||||
* Determines how Scramble stores the names of enum cases.
|
||||
* Available options:
|
||||
* - 'names' – Case names are stored in the `x-enumNames` enum schema extension.
|
||||
* - 'varnames' - Case names are stored in the `x-enum-varnames` enum schema extension.
|
||||
* - false - Case names are not stored.
|
||||
*/
|
||||
'enum_cases_names_strategy' => false,
|
||||
|
||||
/**
|
||||
* When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated
|
||||
* OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until
|
||||
* OpenAPI 3.2 is released and query string structure can be described properly.
|
||||
*
|
||||
* For example, this nested validation rule describes the object with `bar` property:
|
||||
* `['foo.bar' => ['required', 'int']]`.
|
||||
*
|
||||
* When `flatten_deep_query_parameters` is `true`, Scramble will document the parameter like so:
|
||||
* `{"name":"foo[bar]", "schema":{"type":"int"}, "required":true}`.
|
||||
*
|
||||
* When `flatten_deep_query_parameters` is `false`, Scramble will document the parameter like so:
|
||||
* `{"name":"foo", "schema": {"type":"object", "properties":{"bar":{"type": "int"}}, "required": ["bar"]}, "required":true}`.
|
||||
*/
|
||||
'flatten_deep_query_parameters' => true,
|
||||
|
||||
'middleware' => [
|
||||
'web',
|
||||
RestrictedDocsAccess::class,
|
||||
],
|
||||
|
||||
'extensions' => [],
|
||||
|
||||
/*
|
||||
* Automatically document API security (OpenAPI `security` / `securitySchemes`) based on route
|
||||
* middleware.
|
||||
*
|
||||
* Disabled by default. Uncomment the line below to enable `MiddlewareAuthSecurityStrategy`.
|
||||
* When at least one documented route uses middleware matching the configured patterns (by default
|
||||
* `auth` and `auth:*`), bearer auth is applied globally. Routes without matching middleware are
|
||||
* marked as public (`security: []`).
|
||||
*
|
||||
* Set to `null` explicitly to disable. If you already configure security manually via
|
||||
* `afterOpenApiGenerated` / `extendOpenApi`, keep this disabled to avoid duplicate schemes.
|
||||
*
|
||||
* Customize with a class-string or [class, options]:
|
||||
*
|
||||
* 'security_strategy' => [
|
||||
* \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
|
||||
* [
|
||||
* 'middleware' => ['auth', 'auth:*'],
|
||||
* 'scheme' => \Dedoc\Scramble\Support\Generator\SecurityScheme::http('bearer'),
|
||||
* ],
|
||||
* ],
|
||||
*/
|
||||
// 'security_strategy' => \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
|
||||
'security_strategy' => null,
|
||||
];
|
||||
+10
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Dedoc\Scramble\Infer\Scope\Index;
|
||||
use Dedoc\Scramble\Infer\Services\FileParser;
|
||||
use PhpParser\ParserFactory;
|
||||
|
||||
include 'vendor/autoload.php';
|
||||
|
||||
$classes = [
|
||||
Exception::class,
|
||||
RuntimeException::class,
|
||||
\Symfony\Component\HttpKernel\Exception\HttpException::class,
|
||||
];
|
||||
|
||||
app()->singleton(FileParser::class, function () {
|
||||
return new FileParser(
|
||||
(new ParserFactory)->createForHostVersion()
|
||||
);
|
||||
});
|
||||
app()->singleton(Index::class);
|
||||
|
||||
$classesDefinitions = [];
|
||||
foreach ($classes as $className) {
|
||||
$classesDefinitions[$className] = generateClassDefinitionInitialization($className);
|
||||
}
|
||||
|
||||
function generateClassDefinitionInitialization(string $name)
|
||||
{
|
||||
$classAnalyzer = app(\Dedoc\Scramble\Infer\Analyzer\ClassAnalyzer::class);
|
||||
|
||||
$classDefinition = $classAnalyzer->analyze($name);
|
||||
foreach ($classDefinition->methods as $methodName => $method) {
|
||||
$classDefinition->getMethodDefinition($methodName);
|
||||
}
|
||||
|
||||
return serialize($classAnalyzer->analyze($name));
|
||||
}
|
||||
|
||||
$def = var_export($classesDefinitions, true);
|
||||
file_put_contents(__DIR__.'/../classMap.php', <<<EOL
|
||||
<?php
|
||||
/*
|
||||
* Do not change! This file is generated via scripts/generate.php.
|
||||
*/
|
||||
return {$def};
|
||||
EOL);
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
includes:
|
||||
- phpstan-baseline.neon
|
||||
|
||||
parameters:
|
||||
level: 9
|
||||
paths:
|
||||
- src
|
||||
- config
|
||||
stubFiles:
|
||||
- stubs/DeepCopy.stub
|
||||
tmpDir: build/phpstan
|
||||
checkOctaneCompatibility: true
|
||||
checkModelProperties: true
|
||||
typeAliases:
|
||||
Rule: 'string|\Illuminate\Contracts\Validation\ValidationRule'
|
||||
RuleSet: 'Rule|Rule[]'
|
||||
#reportUnmatchedIgnoredErrors: false
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"preset": "laravel",
|
||||
"rules": {
|
||||
"fully_qualified_strict_types": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="{{ $config->renderer()->get('theme', 'light') }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="color-scheme" content="{{ $config->renderer()->get('theme', 'light') }}">
|
||||
<title>{{ $config->get('ui.title') ?? config('app.name') . ' - API Docs' }}</title>
|
||||
|
||||
<script src="https://unpkg.com/@stoplight/elements@8.4.2/web-components.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements@8.4.2/styles.min.css">
|
||||
|
||||
<script>
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
// intercept TryIt requests and add the XSRF-TOKEN header,
|
||||
// which is necessary for Sanctum cookie-based authentication to work correctly
|
||||
window.fetch = (url, options) => {
|
||||
const CSRF_TOKEN_COOKIE_KEY = "XSRF-TOKEN";
|
||||
const CSRF_TOKEN_HEADER_KEY = "X-XSRF-TOKEN";
|
||||
const getCookieValue = (key) => {
|
||||
const cookie = document.cookie.split(';').find((cookie) => cookie.trim().startsWith(key));
|
||||
return cookie?.split("=")[1];
|
||||
};
|
||||
|
||||
const updateFetchHeaders = (
|
||||
headers,
|
||||
headerKey,
|
||||
headerValue,
|
||||
) => {
|
||||
if (headers instanceof Headers) {
|
||||
headers.set(headerKey, headerValue);
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.push([headerKey, headerValue]);
|
||||
} else if (headers) {
|
||||
headers[headerKey] = headerValue;
|
||||
}
|
||||
};
|
||||
const csrfToken = getCookieValue(CSRF_TOKEN_COOKIE_KEY);
|
||||
if (csrfToken) {
|
||||
const { headers = new Headers() } = options || {};
|
||||
updateFetchHeaders(headers, CSRF_TOKEN_HEADER_KEY, decodeURIComponent(csrfToken));
|
||||
return originalFetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return originalFetch(url, options);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { margin:0; height:100%; }
|
||||
body { background-color: var(--color-canvas); }
|
||||
/* issues about the dark theme of stoplight/mosaic-code-viewer using web component:
|
||||
* https://github.com/stoplightio/elements/issues/2188#issuecomment-1485461965
|
||||
*/
|
||||
[data-theme="dark"] .token.property {
|
||||
color: rgb(128, 203, 196) !important;
|
||||
}
|
||||
[data-theme="dark"] .token.operator {
|
||||
color: rgb(255, 123, 114) !important;
|
||||
}
|
||||
[data-theme="dark"] .token.number {
|
||||
color: rgb(247, 140, 108) !important;
|
||||
}
|
||||
[data-theme="dark"] .token.string {
|
||||
color: rgb(165, 214, 255) !important;
|
||||
}
|
||||
[data-theme="dark"] .token.boolean {
|
||||
color: rgb(121, 192, 255) !important;
|
||||
}
|
||||
[data-theme="dark"] .token.punctuation {
|
||||
color: #dbdbdb !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="height: 100vh; overflow-y: hidden">
|
||||
<elements-api
|
||||
id="docs"
|
||||
@foreach($config->renderer()->all(except: ['theme']) as $key => $value)
|
||||
@continue(! $value)
|
||||
{{ $key }}="{{ $value === true ? 'true' : ($value === false ? 'false' : $value) }}"
|
||||
@endforeach
|
||||
/>
|
||||
<script>
|
||||
(async () => {
|
||||
const docs = document.getElementById('docs');
|
||||
docs.apiDescriptionDocument = @json($spec);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@if($config->renderer()->get('theme', 'light') === 'system')
|
||||
<script>
|
||||
var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
function updateTheme(e) {
|
||||
if (e.matches) {
|
||||
window.document.documentElement.setAttribute('data-theme', 'dark');
|
||||
window.document.getElementsByName('color-scheme')[0].setAttribute('content', 'dark');
|
||||
} else {
|
||||
window.document.documentElement.setAttribute('data-theme', 'light');
|
||||
window.document.getElementsByName('color-scheme')[0].setAttribute('content', 'light');
|
||||
}
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener('change', updateTheme);
|
||||
updateTheme(mediaQuery);
|
||||
</script>
|
||||
@endif
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>{{ $config->get('ui.title') ?? config('app.name') . ' - API Docs' }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="{{ $config->renderer()->get('cdn', 'https://cdn.jsdelivr.net/npm/@scalar/api-reference') }}"></script>
|
||||
|
||||
<script>
|
||||
const CSRF_TOKEN_COOKIE_KEY = "XSRF-TOKEN";
|
||||
const CSRF_TOKEN_HEADER_KEY = "X-XSRF-TOKEN";
|
||||
const getCookieValue = (key) => {
|
||||
const cookie = document.cookie.split(';').find((cookie) => cookie.trim().startsWith(key));
|
||||
return cookie?.split("=")[1];
|
||||
};
|
||||
|
||||
Scalar.createApiReference('#app', {
|
||||
content: @json($spec),
|
||||
...@json($config->renderer()->all(except: ['cdn', 'credentials'])),
|
||||
onBeforeRequest: ({ requestBuilder }) => {
|
||||
requestBuilder.headers.set(CSRF_TOKEN_HEADER_KEY, decodeURIComponent(getCookieValue(CSRF_TOKEN_COOKIE_KEY)))
|
||||
},
|
||||
customFetch: (input, init) => {
|
||||
return window.fetch(input, { ...init, credentials: @json($config->renderer()->get('credentials', 'include')) })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Dedoc\Scramble\Scramble;
|
||||
|
||||
Scramble::registerUiRoute(path: 'docs/api')->name('scramble.docs.ui');
|
||||
|
||||
Scramble::registerJsonSpecificationRoute(path: 'docs/api.json')->name('scramble.docs.document');
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble;
|
||||
|
||||
class AbstractOpenApiVisitor implements OpenApiVisitor
|
||||
{
|
||||
public function enter(mixed $object, array $path = []) {}
|
||||
|
||||
public function leave(mixed $object, array $path = []) {}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class BodyParameter extends Parameter
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
?string $description = null,
|
||||
?bool $required = null,
|
||||
$deprecated = false,
|
||||
?string $type = null,
|
||||
?string $format = null,
|
||||
bool $infer = true,
|
||||
mixed $default = new MissingValue,
|
||||
mixed $example = new MissingValue,
|
||||
array $examples = [],
|
||||
) {
|
||||
parent::__construct('body', $name, $description, $required, $deprecated, $type, $format, $infer, $default, $example, $examples);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class CookieParameter extends Parameter
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
?string $description = null,
|
||||
?bool $required = null,
|
||||
$deprecated = false,
|
||||
?string $type = null,
|
||||
?string $format = null,
|
||||
bool $infer = true,
|
||||
mixed $default = new MissingValue,
|
||||
mixed $example = new MissingValue,
|
||||
array $examples = [],
|
||||
) {
|
||||
parent::__construct('cookie', $name, $description, $required, $deprecated, $type, $format, $infer, $default, $example, $examples);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Adds metadata to endpoints.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
|
||||
class Endpoint
|
||||
{
|
||||
public function __construct(
|
||||
/**
|
||||
* Assigns an OperationID to a controller method.
|
||||
*/
|
||||
public readonly ?string $operationId = null,
|
||||
/**
|
||||
* Sets the title (summary) of the endpoint.
|
||||
*/
|
||||
public readonly ?string $title = null,
|
||||
/**
|
||||
* Sets the description of the endpoint.
|
||||
*/
|
||||
public readonly ?string $description = null,
|
||||
/**
|
||||
* Allows to override the method of the endpoint in the documentation. Useful when you want to document the
|
||||
* `PUT|PATCH` endpoint as `PATCH`. The method provided here MUST be the actual method the API will reply to.
|
||||
*/
|
||||
public readonly ?string $method = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Dedoc\Scramble\Support\Generator\Example as OpenApiExample;
|
||||
use Dedoc\Scramble\Support\Generator\MissingValue as OpenApiMissingValue;
|
||||
|
||||
class Example
|
||||
{
|
||||
public function __construct(
|
||||
public mixed $value = new MissingValue,
|
||||
public ?string $summary = null,
|
||||
public ?string $description = null,
|
||||
public ?string $externalValue = null,
|
||||
) {}
|
||||
|
||||
public static function toOpenApiExample(Example $example): OpenApiExample
|
||||
{
|
||||
return new OpenApiExample(
|
||||
value: $example->value instanceof MissingValue ? new OpenApiMissingValue : $example->value,
|
||||
summary: $example->summary,
|
||||
description: $example->description,
|
||||
externalValue: $example->externalValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Excludes all routes of a controller from the API documentation. Applies to controller's methods.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class ExcludeAllRoutesFromDocs {}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Excludes a route from API documentation. Applies to controller's methods.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
|
||||
class ExcludeRouteFromDocs {}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Groups endpoints.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
|
||||
class Group
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ?string $name = null,
|
||||
public readonly ?string $description = null,
|
||||
|
||||
/**
|
||||
* Determines the ordering of the groups. Groups with the same weight, are sorted
|
||||
* by the name (with `SORT_LOCALE_STRING` sorting flag).
|
||||
*/
|
||||
public readonly int $weight = PHP_INT_MAX,
|
||||
) {}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
|
||||
use Dedoc\Scramble\Support\Generator\Header as OpenApiHeader;
|
||||
use Dedoc\Scramble\Support\Generator\MissingValue as OpenApiMissingValue;
|
||||
use Dedoc\Scramble\Support\Generator\Schema;
|
||||
use Dedoc\Scramble\Support\Generator\Types\MixedType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\StringType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\Type as OpenApiType;
|
||||
use Dedoc\Scramble\Support\Generator\TypeTransformer;
|
||||
use Dedoc\Scramble\Support\PhpDoc;
|
||||
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
|
||||
class Header
|
||||
{
|
||||
/**
|
||||
* @param scalar|array<mixed>|object|MissingValue $example
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly ?string $description = null,
|
||||
public readonly ?string $type = null,
|
||||
public readonly ?string $format = null,
|
||||
public readonly ?bool $required = null,
|
||||
/** @var array<mixed>|scalar|null|MissingValue */
|
||||
public readonly mixed $default = new MissingValue,
|
||||
public readonly mixed $example = new MissingValue,
|
||||
/** @var array<string, Example> $examples */
|
||||
public readonly array $examples = [],
|
||||
public readonly ?bool $deprecated = null,
|
||||
public readonly ?bool $explode = null,
|
||||
public readonly int|string $status = 200,
|
||||
) {}
|
||||
|
||||
public static function toOpenApiHeader(Header $header, TypeTransformer $openApiTransformer): OpenApiHeader
|
||||
{
|
||||
$type = self::getType($header, $openApiTransformer);
|
||||
$default = $header->default instanceof MissingValue ? new OpenApiMissingValue : $header->default;
|
||||
$format = $header->format ?: '';
|
||||
|
||||
if ($type instanceof MixedType && ($format || ! $default instanceof OpenApiMissingValue)) {
|
||||
$type = new StringType;
|
||||
}
|
||||
|
||||
return new OpenApiHeader(
|
||||
description: $header->description,
|
||||
required: $header->required,
|
||||
deprecated: $header->deprecated,
|
||||
explode: $header->explode,
|
||||
schema: Schema::fromType(
|
||||
$type->default($default)->format($format)
|
||||
),
|
||||
example: $header->example instanceof MissingValue ? new OpenApiMissingValue : $header->example,
|
||||
examples: array_map(fn ($e) => Example::toOpenApiExample($e), $header->examples),
|
||||
);
|
||||
}
|
||||
|
||||
public static function getType(Header $header, TypeTransformer $openApiTransformer): OpenApiType
|
||||
{
|
||||
return $header->type ? $openApiTransformer->transform(
|
||||
PhpDocTypeHelper::toType(
|
||||
PhpDoc::parse("/** @return $header->type */")->getReturnTagValues()[0]->type ?? new IdentifierTypeNode('mixed')
|
||||
)
|
||||
) : new MixedType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class HeaderParameter extends Parameter
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
?string $description = null,
|
||||
?bool $required = null,
|
||||
$deprecated = false,
|
||||
?string $type = null,
|
||||
?string $format = null,
|
||||
bool $infer = true,
|
||||
mixed $default = new MissingValue,
|
||||
mixed $example = new MissingValue,
|
||||
array $examples = [],
|
||||
) {
|
||||
parent::__construct('header', $name, $description, $required, $deprecated, $type, $format, $infer, $default, $example, $examples);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Marks a class property as hidden from OpenAPI object schema.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||
class Hidden {}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
|
||||
class IgnoreParam
|
||||
{
|
||||
/**
|
||||
* @param 'query'|'path'|'header'|'cookie'|'body'|null $in
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly ?string $in = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
class MissingValue {}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class Parameter
|
||||
{
|
||||
public readonly bool $required;
|
||||
|
||||
/**
|
||||
* @param 'query'|'path'|'header'|'cookie'|'body' $in
|
||||
* @param scalar|array|object|MissingValue $example
|
||||
* @param array<string, Example> $examples The key is a distinct name and the value is an example object.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $in,
|
||||
public readonly string $name,
|
||||
public readonly ?string $description = null,
|
||||
?bool $required = null,
|
||||
public bool $deprecated = false,
|
||||
public ?string $type = null,
|
||||
public ?string $format = null,
|
||||
public bool $infer = true,
|
||||
public mixed $default = new MissingValue,
|
||||
public mixed $example = new MissingValue,
|
||||
public array $examples = [],
|
||||
) {
|
||||
$this->required = $required !== null ? $required : $this->in === 'path';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class PathParameter extends Parameter
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
?string $description = null,
|
||||
?bool $required = null,
|
||||
$deprecated = false,
|
||||
?string $type = null,
|
||||
?string $format = null,
|
||||
bool $infer = true,
|
||||
mixed $default = new MissingValue,
|
||||
mixed $example = new MissingValue,
|
||||
array $examples = [],
|
||||
) {
|
||||
parent::__construct('path', $name, $description, $required, $deprecated, $type, $format, $infer, $default, $example, $examples);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
|
||||
class QueryParameter extends Parameter
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
?string $description = null,
|
||||
?bool $required = null,
|
||||
$deprecated = false,
|
||||
?string $type = null,
|
||||
?string $format = null,
|
||||
bool $infer = true,
|
||||
mixed $default = new MissingValue,
|
||||
mixed $example = new MissingValue,
|
||||
array $examples = [],
|
||||
) {
|
||||
parent::__construct('query', $name, $description, $required, $deprecated, $type, $format, $infer, $default, $example, $examples);
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
use Dedoc\Scramble\Infer\Services\FileNameResolver;
|
||||
use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
|
||||
use Dedoc\Scramble\Support\Generator\Reference;
|
||||
use Dedoc\Scramble\Support\Generator\Response as OpenApiResponse;
|
||||
use Dedoc\Scramble\Support\Generator\Schema;
|
||||
use Dedoc\Scramble\Support\Generator\Types\StringType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\Type;
|
||||
use Dedoc\Scramble\Support\Generator\TypeTransformer;
|
||||
use Dedoc\Scramble\Support\PhpDoc;
|
||||
use Illuminate\Support\Str;
|
||||
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
|
||||
|
||||
use function DeepCopy\deep_copy;
|
||||
|
||||
/**
|
||||
* @phpstan-type BaseExample array<mixed>|scalar|null
|
||||
*/
|
||||
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
|
||||
class Response
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int|string $status = 200,
|
||||
public readonly ?string $description = null,
|
||||
public readonly string $mediaType = 'application/json',
|
||||
public readonly ?string $type = null,
|
||||
public readonly ?string $format = null,
|
||||
/** @var BaseExample|BaseExample[] $examples */
|
||||
public readonly mixed $examples = [],
|
||||
) {}
|
||||
|
||||
public static function toOpenApiResponse(Response $responseAttribute, ?OpenApiResponse $originalResponse, TypeTransformer $openApiTransformer, ?FileNameResolver $nameResolver): OpenApiResponse
|
||||
{
|
||||
$response = $originalResponse ? deep_copy($originalResponse) : OpenApiResponse::make($responseAttribute->status);
|
||||
|
||||
$response = self::applyResponseMediaType($responseAttribute, $response, $openApiTransformer, $nameResolver);
|
||||
|
||||
$response->setDescription(self::getDescription($responseAttribute, $response));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private static function applyResponseMediaType(Response $responseAttribute, OpenApiResponse $response, TypeTransformer $openApiTransformer, ?FileNameResolver $nameResolver): OpenApiResponse
|
||||
{
|
||||
if (! $responseAttribute->type) {
|
||||
return $response
|
||||
->setContent(
|
||||
$responseAttribute->mediaType,
|
||||
self::getMediaType($responseAttribute, $response),
|
||||
);
|
||||
}
|
||||
|
||||
$responseFromType = $openApiTransformer->toResponse(
|
||||
PhpDocTypeHelper::toType(
|
||||
PhpDoc::parse("/** @return $responseAttribute->type */", $nameResolver)->getReturnTagValues()[0]->type ?? new IdentifierTypeNode('mixed')
|
||||
)
|
||||
);
|
||||
|
||||
if ($responseFromType instanceof Reference) {
|
||||
$responseFromType = deep_copy($responseFromType->resolve());
|
||||
}
|
||||
|
||||
/** @var OpenApiResponse|null $responseFromType */
|
||||
if (! $responseFromType) {
|
||||
return $response->setContent(
|
||||
$responseAttribute->mediaType,
|
||||
self::getMediaType($responseAttribute, $response),
|
||||
);
|
||||
}
|
||||
|
||||
return $response
|
||||
->setDescription($responseFromType->description ?: self::getDescription($responseAttribute, $response))
|
||||
->setContent(
|
||||
$responseAttribute->mediaType,
|
||||
self::getMediaType($responseAttribute, $responseFromType),
|
||||
);
|
||||
}
|
||||
|
||||
private static function getDescription(Response $responseAttribute, OpenApiResponse $response): string
|
||||
{
|
||||
if ($responseAttribute->description === null) {
|
||||
return $response->description;
|
||||
}
|
||||
|
||||
return Str::replace('$0', $response->description, $responseAttribute->description);
|
||||
}
|
||||
|
||||
private static function getMediaType(Response $responseAttribute, OpenApiResponse $response): Schema|Reference
|
||||
{
|
||||
return self::getSchema($responseAttribute, $response->content[$responseAttribute->mediaType] ?? null);
|
||||
}
|
||||
|
||||
private static function getSchema(Response $responseAttribute, Schema|Reference|null $schema): Schema|Reference
|
||||
{
|
||||
if (! $responseAttribute->format && ! $responseAttribute->examples) {
|
||||
return $schema ?: Schema::fromType(new StringType);
|
||||
}
|
||||
|
||||
$schemaType = $schema
|
||||
? clone ($schema instanceof Reference ? $schema->resolve()->type : $schema->type)
|
||||
: new StringType;
|
||||
|
||||
/** @var Type $schemaType */
|
||||
if ($responseAttribute->format) {
|
||||
$schemaType->format($responseAttribute->format);
|
||||
}
|
||||
|
||||
if ($responseAttribute->examples) {
|
||||
$schemaType->examples($responseAttribute->examples); // @phpstan-ignore argument.type
|
||||
}
|
||||
|
||||
return Schema::fromType($schemaType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Attributes;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Allows naming class based schemas for different contexts.
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class SchemaName
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
/**
|
||||
* Some classes can be used both as input and output schemas. So this property is used to
|
||||
* explicitly name the schema when is in input context.
|
||||
*/
|
||||
public readonly ?string $input = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble;
|
||||
|
||||
class CacheableGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private Generator $generator,
|
||||
) {}
|
||||
|
||||
public function __invoke(?GeneratorConfig $config = null): array
|
||||
{
|
||||
$config ??= Scramble::getGeneratorConfig(Scramble::DEFAULT_API);
|
||||
|
||||
$store = config('scramble.cache.store');
|
||||
$keyBase = config('scramble.cache.key');
|
||||
|
||||
if (! $store || ! $keyBase) {
|
||||
return ($this->generator)($config);
|
||||
}
|
||||
|
||||
$key = $keyBase.':'.$this->resolveApi($config);
|
||||
|
||||
if ($cached = cache()->store($store)->get($key)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
return ($this->generator)($config);
|
||||
}
|
||||
|
||||
private function resolveApi(GeneratorConfig $config): string
|
||||
{
|
||||
foreach (Scramble::getConfigurationsInstance()->all() as $api => $generatorConfig) {
|
||||
if ($generatorConfig === $config) {
|
||||
return $api;
|
||||
}
|
||||
}
|
||||
|
||||
return Scramble::DEFAULT_API;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ApiPath
|
||||
{
|
||||
/**
|
||||
* @param list<string> $includes
|
||||
* @param list<string> $excludes
|
||||
*/
|
||||
private function __construct(
|
||||
private readonly array $includes,
|
||||
private readonly array $excludes,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string|array{include?: string|string[], exclude?: string|string[]}|null $config
|
||||
*/
|
||||
public static function from(mixed $config, string $default = 'api'): self
|
||||
{
|
||||
if ($config === null || is_string($config)) {
|
||||
return new self(self::normalizePrefixes($config ?? $default), []);
|
||||
}
|
||||
|
||||
if (is_array($config) && (array_key_exists('include', $config) || array_key_exists('exclude', $config))) {
|
||||
return new self(
|
||||
self::normalizePrefixes($config['include'] ?? $default),
|
||||
self::normalizePrefixes($config['exclude'] ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid scramble.api_path config. Expected a string or an array with `include` and/or `exclude` keys.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function normalizePrefixes(mixed $prefixes): array
|
||||
{
|
||||
if ($prefixes === '' || $prefixes === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(Arr::wrap($prefixes), fn ($prefix) => is_string($prefix) && $prefix !== ''));
|
||||
}
|
||||
|
||||
public function matches(string $uri): bool
|
||||
{
|
||||
if ($this->includes !== [] && ! $this->matchesAny($uri, $this->includes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! $this->matchesAny($uri, $this->excludes);
|
||||
}
|
||||
|
||||
public function stripPrefix(string $uri): string
|
||||
{
|
||||
if (! $this->usesSingleBase()) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
return (string) Str::of($uri)
|
||||
->replaceStart($this->includes[0], '')
|
||||
->trim('/');
|
||||
}
|
||||
|
||||
public function serverPath(): string
|
||||
{
|
||||
return $this->usesSingleBase() ? $this->includes[0] : '';
|
||||
}
|
||||
|
||||
private function usesSingleBase(): bool
|
||||
{
|
||||
return count($this->includes) === 1 && ! str_contains($this->includes[0], '*');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $patterns
|
||||
*/
|
||||
private function matchesAny(string $uri, array $patterns): bool
|
||||
{
|
||||
foreach ($patterns as $pattern) {
|
||||
if (str_contains($pattern, '*') ? Str::is($pattern, $uri) : ($uri === $pattern || Str::startsWith($uri, $pattern.'/'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\Contracts\DocumentTransformer;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class DocumentTransformers
|
||||
{
|
||||
protected array $transformers = [];
|
||||
|
||||
protected array $appends = [];
|
||||
|
||||
protected array $prepends = [];
|
||||
|
||||
public function append(array|callable|string $transformers)
|
||||
{
|
||||
$this->appends = array_merge(
|
||||
$this->appends,
|
||||
Arr::wrap($transformers)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function prepend(array|callable|string $transformers)
|
||||
{
|
||||
$this->prepends = array_merge(
|
||||
$this->prepends,
|
||||
Arr::wrap($transformers)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function use(array $transformers)
|
||||
{
|
||||
$this->transformers = $transformers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (callable|class-string<DocumentTransformer>)[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$base = $this->transformers;
|
||||
|
||||
return array_values(array_unique([
|
||||
...$this->prepends,
|
||||
...$base,
|
||||
...$this->appends,
|
||||
], SORT_REGULAR));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\GeneratorConfig;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Illuminate\Routing\Router;
|
||||
use LogicException;
|
||||
|
||||
class GeneratorConfigCollection
|
||||
{
|
||||
/**
|
||||
* @var array<string, GeneratorConfig>
|
||||
*/
|
||||
private array $apis = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apis[Scramble::DEFAULT_API] = $this->buildDefaultApiConfiguration();
|
||||
}
|
||||
|
||||
private function buildDefaultApiConfiguration(): GeneratorConfig
|
||||
{
|
||||
return (new GeneratorConfig)
|
||||
->expose(
|
||||
ui: fn (Router $router, $action) => $router->get('docs/api', $action)->name('scramble.docs.ui'),
|
||||
document: fn (Router $router, $action) => $router->get('docs/api.json', $action)->name('scramble.docs.document'),
|
||||
);
|
||||
}
|
||||
|
||||
public function get(string $name): GeneratorConfig
|
||||
{
|
||||
if (! array_key_exists($name, $this->apis)) {
|
||||
throw new LogicException("$name API is not registered. Register the API using `Scramble::registerApi` first.");
|
||||
}
|
||||
|
||||
return $this->apis[$name];
|
||||
}
|
||||
|
||||
public function register(string $name, array $config): GeneratorConfig
|
||||
{
|
||||
$this->apis[$name] = $generatorConfig = $this->apis[Scramble::DEFAULT_API]
|
||||
->cloneWithoutExposing()
|
||||
->useConfig(array_merge(config('scramble') ?: [], $config));
|
||||
|
||||
return $generatorConfig;
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
return $this->apis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\Infer\Configuration\ClassLike;
|
||||
use Dedoc\Scramble\Infer\Configuration\DefinitionMatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class InferConfig
|
||||
{
|
||||
/** @var DefinitionMatcher[] */
|
||||
private array $forcedAstSourcedDefinitionsMatchers = [];
|
||||
|
||||
/** @var DefinitionMatcher[] */
|
||||
private array $forcedReflectionSourcedDefinitionsMatchers = [];
|
||||
|
||||
/**
|
||||
* @param (string|DefinitionMatcher)[] $items
|
||||
* @return $this
|
||||
*/
|
||||
public function buildDefinitionsUsingAstFor(array $items): static
|
||||
{
|
||||
$this->forcedAstSourcedDefinitionsMatchers = [
|
||||
...$this->forcedAstSourcedDefinitionsMatchers,
|
||||
...array_map(
|
||||
fn ($item) => is_string($item) ? new ClassLike($item) : $item,
|
||||
Arr::wrap($items),
|
||||
),
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (string|DefinitionMatcher)[] $items
|
||||
* @return $this
|
||||
*/
|
||||
public function buildDefinitionsUsingReflectionFor(array $items): static
|
||||
{
|
||||
$this->forcedReflectionSourcedDefinitionsMatchers = [
|
||||
...$this->forcedReflectionSourcedDefinitionsMatchers,
|
||||
...array_map(
|
||||
fn ($item) => is_string($item) ? new ClassLike($item) : $item,
|
||||
Arr::wrap($items),
|
||||
),
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DefinitionMatcher[] $matchers
|
||||
*/
|
||||
private function matchesAnyDefinitionMatcher(string $class, array $matchers): bool
|
||||
{
|
||||
foreach ($matchers as $item) {
|
||||
if ($item->matches($class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function shouldAnalyzeAst(string $class): bool
|
||||
{
|
||||
if ($this->matchesAnyDefinitionMatcher($class, $this->forcedReflectionSourcedDefinitionsMatchers)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->matchesAnyDefinitionMatcher($class, $this->forcedAstSourcedDefinitionsMatchers)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ignoring static analysis error due to it is fine to exception to be thrown here if bad input.
|
||||
$reflection = new \ReflectionClass($class); // @phpstan-ignore argument.type
|
||||
|
||||
$path = $reflection->getFileName();
|
||||
|
||||
if (! $path) {
|
||||
return true; // Keep in mind the internal classes are analyzed via AST analyzer
|
||||
}
|
||||
|
||||
return ! Str::contains($path, DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\Enums\JsonApiArraySerialization;
|
||||
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
|
||||
|
||||
class JsonApiConfig
|
||||
{
|
||||
public function __construct(
|
||||
public readonly JsonApiArraySerialization $arraySerialization = JsonApiArraySerialization::Comma,
|
||||
public readonly ?int $maxRelationshipDepth = null,
|
||||
) {}
|
||||
|
||||
/** @return non-negative-int */
|
||||
public function maxRelationshipDepth(): int
|
||||
{
|
||||
if ($this->maxRelationshipDepth === null) {
|
||||
return JsonApiResource::$maxRelationshipDepth; // @phpstan-ignore return.type
|
||||
}
|
||||
|
||||
return max(0, min($this->maxRelationshipDepth, JsonApiResource::$maxRelationshipDepth));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\Contracts\OperationTransformer;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\DeprecationExtension;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ErrorResponsesExtension;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\RequestBodyExtension;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\RequestEssentialsExtension;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ResponseExtension;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ResponseHeadersExtension;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class OperationTransformers
|
||||
{
|
||||
protected array $transformers = [];
|
||||
|
||||
protected array $appends = [];
|
||||
|
||||
protected array $prepends = [];
|
||||
|
||||
public function append(array|callable|string $transformers)
|
||||
{
|
||||
$this->appends = array_merge(
|
||||
$this->appends,
|
||||
Arr::wrap($transformers)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function prepend(array|callable|string $transformers)
|
||||
{
|
||||
$this->prepends = array_merge(
|
||||
$this->prepends,
|
||||
Arr::wrap($transformers)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function use(array $transformers)
|
||||
{
|
||||
$this->transformers = $transformers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<callable|class-string<OperationTransformer>>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$base = $this->transformers ?: [
|
||||
RequestEssentialsExtension::class,
|
||||
RequestBodyExtension::class,
|
||||
ErrorResponsesExtension::class,
|
||||
ResponseExtension::class,
|
||||
ResponseHeadersExtension::class,
|
||||
DeprecationExtension::class,
|
||||
];
|
||||
|
||||
return array_values(array_unique([
|
||||
...$this->prepends,
|
||||
...$base,
|
||||
...$this->appends,
|
||||
], SORT_REGULAR));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\AttributesParametersExtractor;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\FormRequestParametersExtractor;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\JsonApiResourceParametersExtractor;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\MethodCallsParametersExtractor;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\ParameterExtractor;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\PathParametersExtractor;
|
||||
use Dedoc\Scramble\Support\OperationExtensions\ParameterExtractor\ValidateCallParametersExtractor;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ParametersExtractors
|
||||
{
|
||||
/** @var class-string<ParameterExtractor>[] */
|
||||
protected array $extractors = [];
|
||||
|
||||
/** @var class-string<ParameterExtractor>[] */
|
||||
protected array $appends = [];
|
||||
|
||||
/** @var class-string<ParameterExtractor>[] */
|
||||
protected array $prepends = [];
|
||||
|
||||
/**
|
||||
* @param class-string<ParameterExtractor>[]|class-string<ParameterExtractor> $extractor
|
||||
* @return $this
|
||||
*/
|
||||
public function append(array|string $extractor): self
|
||||
{
|
||||
$this->appends = array_merge(
|
||||
$this->appends,
|
||||
Arr::wrap($extractor)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<ParameterExtractor>[]|class-string<ParameterExtractor> $extractor
|
||||
* @return $this
|
||||
*/
|
||||
public function prepend(array|string $extractor): self
|
||||
{
|
||||
$this->prepends = array_merge(
|
||||
$this->prepends,
|
||||
Arr::wrap($extractor)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<ParameterExtractor>[] $extractors
|
||||
* @return $this
|
||||
*/
|
||||
public function use(array $extractors): self
|
||||
{
|
||||
$this->extractors = $extractors;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<ParameterExtractor>[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$base = $this->extractors ?: [
|
||||
PathParametersExtractor::class,
|
||||
FormRequestParametersExtractor::class,
|
||||
ValidateCallParametersExtractor::class,
|
||||
JsonApiResourceParametersExtractor::class,
|
||||
];
|
||||
|
||||
$defaultAppends = [
|
||||
MethodCallsParametersExtractor::class,
|
||||
AttributesParametersExtractor::class,
|
||||
];
|
||||
|
||||
return array_values(array_unique([
|
||||
...$this->prepends,
|
||||
...$base,
|
||||
...$this->appends,
|
||||
...$defaultAppends,
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class RendererConfig
|
||||
{
|
||||
public readonly string $view;
|
||||
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* @param array{view: string} $config
|
||||
*/
|
||||
public function __construct(
|
||||
array $config = []
|
||||
) {
|
||||
$this->config = Arr::except($config, ['view']);
|
||||
$this->view = $config['view'];
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return Arr::get($this->config, $key, $default);
|
||||
}
|
||||
|
||||
public function all(array $except = []): array
|
||||
{
|
||||
return Arr::except($this->config, $except);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\Contracts\AllRulesSchemasTransformer;
|
||||
use Dedoc\Scramble\Contracts\RuleTransformer;
|
||||
use Dedoc\Scramble\RuleTransformers\ConfirmedRule;
|
||||
use Dedoc\Scramble\RuleTransformers\EnumRule;
|
||||
use Dedoc\Scramble\RuleTransformers\ExistsRule;
|
||||
use Dedoc\Scramble\RuleTransformers\InRule;
|
||||
use Dedoc\Scramble\RuleTransformers\RegexRule;
|
||||
use Dedoc\Scramble\Support\ContainerUtils;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class RuleTransformers
|
||||
{
|
||||
/** @var list<class-string<RuleTransformer|AllRulesSchemasTransformer>> */
|
||||
protected array $transformers = [];
|
||||
|
||||
/** @var list<class-string<RuleTransformer|AllRulesSchemasTransformer>> */
|
||||
protected array $appends = [];
|
||||
|
||||
/** @var list<class-string<RuleTransformer|AllRulesSchemasTransformer>> */
|
||||
protected array $prepends = [];
|
||||
|
||||
/**
|
||||
* @param class-string<RuleTransformer|AllRulesSchemasTransformer>|list<class-string<RuleTransformer|AllRulesSchemasTransformer>> $transformers
|
||||
*/
|
||||
public function append(array|string $transformers): self
|
||||
{
|
||||
$this->appends = array_values(array_merge( // @phpstan-ignore arrayValues.list
|
||||
$this->appends,
|
||||
Arr::wrap($transformers)
|
||||
));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<RuleTransformer|AllRulesSchemasTransformer>|list<class-string<RuleTransformer|AllRulesSchemasTransformer>> $transformers
|
||||
*/
|
||||
public function prepend(array|string $transformers): self
|
||||
{
|
||||
$this->prepends = array_values(array_merge( // @phpstan-ignore arrayValues.list
|
||||
$this->prepends,
|
||||
Arr::wrap($transformers)
|
||||
));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<class-string<RuleTransformer|AllRulesSchemasTransformer>> $transformers
|
||||
*/
|
||||
public function use(array $transformers): self
|
||||
{
|
||||
$this->transformers = $transformers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TExtensionType of object
|
||||
*
|
||||
* @param class-string<TExtensionType> $type
|
||||
* @param array<string, mixed> $contextfulBindings
|
||||
* @return Collection<int, TExtensionType>
|
||||
*/
|
||||
public function instances(string $type, array $contextfulBindings): Collection
|
||||
{
|
||||
return collect($this->all()) // @phpstan-ignore return.type
|
||||
->filter(fn ($class) => is_a($class, $type, true))
|
||||
->map(fn ($class) => ContainerUtils::makeContextable($class, $contextfulBindings))
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<class-string<RuleTransformer|AllRulesSchemasTransformer>>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
$base = $this->transformers ?: [
|
||||
EnumRule::class,
|
||||
InRule::class,
|
||||
ConfirmedRule::class,
|
||||
ExistsRule::class,
|
||||
RegexRule::class,
|
||||
];
|
||||
|
||||
return array_values(array_unique([
|
||||
...$this->prepends,
|
||||
...$base,
|
||||
...$this->appends,
|
||||
], SORT_REGULAR));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\GeneratorConfig;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SecurityDocumentationContext
|
||||
{
|
||||
/**
|
||||
* @param Collection<int, \Illuminate\Routing\Route> $routes
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly Collection $routes,
|
||||
public readonly GeneratorConfig $config,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Configuration;
|
||||
|
||||
use Dedoc\Scramble\Support\Generator\ServerVariable;
|
||||
|
||||
class ServerVariables
|
||||
{
|
||||
/**
|
||||
* @param array<string, ServerVariable> $items
|
||||
*/
|
||||
public function __construct(public array $items = []) {}
|
||||
|
||||
public function use(array $items)
|
||||
{
|
||||
$this->items = $items;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function set(string $name, ServerVariable $serverVariable)
|
||||
{
|
||||
$this->items[$name] = $serverVariable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands;
|
||||
|
||||
use Dedoc\Scramble\Console\Commands\Components\TermsOfContentItem;
|
||||
use Dedoc\Scramble\Exceptions\ConsoleRenderable;
|
||||
use Dedoc\Scramble\Exceptions\RouteAware;
|
||||
use Dedoc\Scramble\Generator;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
class AnalyzeDocumentation extends Command
|
||||
{
|
||||
protected $signature = 'scramble:analyze
|
||||
{--api=default : The API to analyze}
|
||||
';
|
||||
|
||||
protected $description = 'Analyzes the documentation generation process to surface any issues.';
|
||||
|
||||
public function handle(Generator $generator): int
|
||||
{
|
||||
$generator->setThrowExceptions(false);
|
||||
|
||||
$generator(Scramble::getGeneratorConfig($this->option('api')));
|
||||
|
||||
$i = 1;
|
||||
$this->groupExceptions($generator->exceptions)->each(function (Collection $exceptions, string $group) use (&$i) {
|
||||
$this->renderExceptionsGroup($exceptions, $group, $i);
|
||||
});
|
||||
|
||||
if (count($generator->exceptions)) {
|
||||
$this->error('[ERROR] Found '.count($generator->exceptions).' errors.');
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Everything is fine! Documentation is generated without any errors 🍻');
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<string, Collection<int, Throwable>>
|
||||
*/
|
||||
private function groupExceptions(array $exceptions): Collection
|
||||
{
|
||||
return collect($exceptions)
|
||||
->groupBy(fn ($e) => $e instanceof RouteAware ? $this->getRouteKey($e->getRoute()) : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Throwable> $exceptions
|
||||
*/
|
||||
private function renderExceptionsGroup(Collection $exceptions, string $group, int &$i): void
|
||||
{
|
||||
// when route key is set, then the exceptions in the group are route aware.
|
||||
if ($group) {
|
||||
$this->renderRouteExceptionsGroupLine($exceptions);
|
||||
}
|
||||
|
||||
$exceptions->each(function ($exception) use (&$i) {
|
||||
$this->renderException($exception, $i);
|
||||
$i++;
|
||||
$this->line('');
|
||||
});
|
||||
}
|
||||
|
||||
private function getRouteKey(?Route $route): string
|
||||
{
|
||||
if (! $route) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$method = implode('|', $route->methods());
|
||||
$action = $route->getAction('uses');
|
||||
|
||||
return "$method.$action";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, RouteAware> $exceptions
|
||||
*/
|
||||
private function renderRouteExceptionsGroupLine(Collection $exceptions): void
|
||||
{
|
||||
$firstException = $exceptions->first();
|
||||
$route = $firstException->getRoute();
|
||||
|
||||
$method = implode('|', $route->methods());
|
||||
$errorsMessage = ($count = $exceptions->count()).' '.Str::plural('error', $count);
|
||||
|
||||
$tocComponent = new TermsOfContentItem(
|
||||
right: '<options=bold;fg='.$this->getHttpMethodColor($method).'>'.$method."</> $route->uri <fg=red>$errorsMessage</>",
|
||||
left: $this->getRouteAction($route),
|
||||
);
|
||||
|
||||
$tocComponent->render($this->output);
|
||||
|
||||
$this->line('');
|
||||
}
|
||||
|
||||
private function getHttpMethodColor(string $method): string
|
||||
{
|
||||
return match ($method) {
|
||||
'POST', 'PUT' => 'blue',
|
||||
'DELETE' => 'red',
|
||||
default => 'yellow',
|
||||
};
|
||||
}
|
||||
|
||||
public function getRouteAction(?Route $route): ?string
|
||||
{
|
||||
if (! $uses = $route->getAction('uses')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count($parts = explode('@', $uses)) !== 2 || ! method_exists(...$parts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$class, $method] = $parts;
|
||||
|
||||
$eloquentClassName = Str::replace(['App\Http\Controllers\\', 'App\Http\\'], '', $class);
|
||||
|
||||
return "<fg=gray>{$eloquentClassName}@{$method}</>";
|
||||
}
|
||||
|
||||
private function renderException(Throwable $exception, int $i): void
|
||||
{
|
||||
$message = Str::replace('Dedoc\Scramble\Support\Generator\Types\\', '', property_exists($exception, 'originalMessage') ? $exception->originalMessage : $exception->getMessage()); // @phpstan-ignore argument.templateType
|
||||
|
||||
$this->output->writeln("<options=bold>$i. {$message}</>");
|
||||
|
||||
if ($exception instanceof ConsoleRenderable) {
|
||||
$exception->renderInConsole($this->output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands;
|
||||
|
||||
use Dedoc\Scramble\Console\Commands\Concerns\ManagesDocumentationCache;
|
||||
use Dedoc\Scramble\Generator;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CacheDocumentation extends Command
|
||||
{
|
||||
use ManagesDocumentationCache;
|
||||
|
||||
protected $signature = 'scramble:cache
|
||||
{--api=* : The API to cache a documentation for (by default, caches all APIs)}
|
||||
';
|
||||
|
||||
protected $description = 'Cache the generated OpenAPI document.';
|
||||
|
||||
public function handle(Generator $generator): int
|
||||
{
|
||||
if (! $this->ensureDocumentationCacheConfigured()) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$store = config('scramble.cache.store');
|
||||
|
||||
foreach ($this->resolveApis() as $api) {
|
||||
$config = Scramble::getGeneratorConfig($api);
|
||||
|
||||
cache()->store($store)->forever(
|
||||
$this->cacheKey($api),
|
||||
$generator($config),
|
||||
);
|
||||
|
||||
$this->info("OpenAPI document cached for [{$api}] API.");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands;
|
||||
|
||||
use Dedoc\Scramble\Console\Commands\Concerns\ManagesDocumentationCache;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearDocumentationCache extends Command
|
||||
{
|
||||
use ManagesDocumentationCache;
|
||||
|
||||
protected $signature = 'scramble:clear
|
||||
{--api=* : The API to clear a documentation cache for (by default, clears all APIs)}
|
||||
';
|
||||
|
||||
protected $description = 'Clear the cached OpenAPI document.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
if (! $this->ensureDocumentationCacheConfigured()) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$store = config('scramble.cache.store');
|
||||
|
||||
foreach ($this->resolveApis() as $api) {
|
||||
cache()->store($store)->forget($this->cacheKey($api));
|
||||
|
||||
$this->info("OpenAPI document cache cleared for [{$api}] API.");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands\Components;
|
||||
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use NunoMaduro\Collision\Highlighter;
|
||||
|
||||
class Code implements Component
|
||||
{
|
||||
public function __construct(
|
||||
public string $filePath,
|
||||
public int $line,
|
||||
) {}
|
||||
|
||||
public function render(OutputStyle $style): void
|
||||
{
|
||||
$code = (new Highlighter)->highlight(file_get_contents($this->filePath), $this->line);
|
||||
|
||||
$style->writeln($code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands\Components;
|
||||
|
||||
use Illuminate\Console\OutputStyle;
|
||||
|
||||
interface Component
|
||||
{
|
||||
public function render(OutputStyle $style): void;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands\Components;
|
||||
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Console\Terminal;
|
||||
|
||||
class TermsOfContentItem implements Component
|
||||
{
|
||||
public function __construct(
|
||||
public string $right,
|
||||
public ?string $left = null,
|
||||
) {}
|
||||
|
||||
public function render(OutputStyle $style): void
|
||||
{
|
||||
$width = (new Terminal)->getWidth();
|
||||
|
||||
$rightWidth = $this->getLineWidth($this->right ?: '');
|
||||
$leftWidth = $this->getLineWidth($this->left ?: '');
|
||||
|
||||
if (! $leftWidth) {
|
||||
$style->writeln($this->right);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$dotsCount = max($width - $rightWidth - $leftWidth - 2, 0);
|
||||
|
||||
$style->writeln("{$this->right}<fg=gray> ".Str::repeat('.', $dotsCount)." </>{$this->left}");
|
||||
}
|
||||
|
||||
private function getLineWidth(string $string)
|
||||
{
|
||||
$re = '/<.*?>/m';
|
||||
|
||||
return mb_strlen(preg_replace($re, '', $string));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands\Concerns;
|
||||
|
||||
use Dedoc\Scramble\Scramble;
|
||||
|
||||
trait ManagesDocumentationCache
|
||||
{
|
||||
protected function ensureDocumentationCacheConfigured(): bool
|
||||
{
|
||||
if (config('scramble.cache.store') === null) {
|
||||
$this->info('Documentation cache store is not configured. Set `scramble.cache.store` in your config.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config('scramble.cache.key') === null) {
|
||||
$this->info('Documentation cache key is not configured. Set `scramble.cache.key` in your config.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
protected function resolveApis(): array
|
||||
{
|
||||
$apis = array_filter($this->option('api') ?: []);
|
||||
|
||||
if ($apis === [] || $apis === ['*']) {
|
||||
return array_keys(Scramble::getConfigurationsInstance()->all());
|
||||
}
|
||||
|
||||
return $apis;
|
||||
}
|
||||
|
||||
protected function cacheKey(string $api): string
|
||||
{
|
||||
return config('scramble.cache.key').':'.$api;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Console\Commands;
|
||||
|
||||
use Dedoc\Scramble\Generator;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class ExportDocumentation extends Command
|
||||
{
|
||||
protected $signature = 'scramble:export
|
||||
{--path= : The path to save the exported JSON file}
|
||||
{--api=default : The API to export a documentation for}
|
||||
';
|
||||
|
||||
protected $description = 'Export the OpenAPI document to a JSON file.';
|
||||
|
||||
public function handle(Generator $generator): void
|
||||
{
|
||||
$api = $this->option('api');
|
||||
$path = $this->option('path');
|
||||
|
||||
$config = Scramble::getGeneratorConfig($api);
|
||||
|
||||
$specification = json_encode($generator($config), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
/** @var string $filename */
|
||||
$filename = $path ?: $config->get('export_path') ?? 'api'.($api === 'default' ? '' : "-$api").'.json';
|
||||
|
||||
File::put($filename, $specification);
|
||||
|
||||
$this->info("OpenAPI document exported to {$filename}.");
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble;
|
||||
|
||||
/** @internal */
|
||||
class ContextReferences
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ContextReferencesCollection $schemas = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $responses = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $parameters = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $examples = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $requestBodies = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $headers = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $securitySchemes = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $links = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $callbacks = new ContextReferencesCollection,
|
||||
public readonly ContextReferencesCollection $pathItems = new ContextReferencesCollection,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble;
|
||||
|
||||
use Dedoc\Scramble\Support\Generator\Reference;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/** @internal */
|
||||
class ContextReferencesCollection
|
||||
{
|
||||
private array $tempNames = [];
|
||||
|
||||
/**
|
||||
* @param array<string, Reference[]> $items The key is reference ID and the value is a list of such references.
|
||||
*/
|
||||
public function __construct(
|
||||
public array $items = [],
|
||||
) {}
|
||||
|
||||
public function has(string $referenceId): bool
|
||||
{
|
||||
return array_key_exists($referenceId, $this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Reference[]
|
||||
*/
|
||||
public function get(string $referenceId): array
|
||||
{
|
||||
return $this->items[$referenceId] ?? [];
|
||||
}
|
||||
|
||||
public function add(string $referenceId, Reference $reference): Reference
|
||||
{
|
||||
$this->items[$referenceId] ??= [];
|
||||
$this->items[$referenceId][] = $this->setUniqueName($reference);
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
public function setUniqueName(Reference $reference): Reference
|
||||
{
|
||||
$reference->fullName = $reference->shortName ?: $this->uniqueSchemaName($reference->fullName);
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
public function uniqueName(string $referenceId): string
|
||||
{
|
||||
if ($this->has($referenceId)) {
|
||||
$reference = $this->get($referenceId)[0];
|
||||
|
||||
return $reference->shortName ?: $reference->fullName;
|
||||
}
|
||||
|
||||
return $this->uniqueSchemaName($referenceId);
|
||||
}
|
||||
|
||||
private function uniqueSchemaName(string $fullName)
|
||||
{
|
||||
$shortestPossibleName = class_basename($fullName);
|
||||
|
||||
if (
|
||||
($this->tempNames[$shortestPossibleName] ?? null) === null
|
||||
|| ($this->tempNames[$shortestPossibleName] ?? null) === $fullName
|
||||
) {
|
||||
$this->tempNames[$shortestPossibleName] = $fullName;
|
||||
|
||||
return static::slug($shortestPossibleName);
|
||||
}
|
||||
|
||||
return static::slug($fullName);
|
||||
}
|
||||
|
||||
private static function slug(string $name)
|
||||
{
|
||||
return Str::replace('\\', '.', $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Contracts;
|
||||
|
||||
use Dedoc\Scramble\Support\RuleTransforming\NormalizedRule;
|
||||
use Dedoc\Scramble\Support\RuleTransforming\RuleTransformerContext;
|
||||
use Dedoc\Scramble\Support\RuleTransforming\SchemaBag;
|
||||
|
||||
interface AllRulesSchemasTransformer
|
||||
{
|
||||
public function shouldHandle(NormalizedRule $rule): bool;
|
||||
|
||||
public function transformAll(SchemaBag $schemaBag, NormalizedRule $rule, RuleTransformerContext $context): void;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Contracts;
|
||||
|
||||
use Dedoc\Scramble\OpenApiContext;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
|
||||
interface DocumentTransformer
|
||||
{
|
||||
public function handle(OpenApi $document, OpenApiContext $context);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Contracts;
|
||||
|
||||
use Dedoc\Scramble\Support\Generator\Operation;
|
||||
use Dedoc\Scramble\Support\RouteInfo;
|
||||
|
||||
interface OperationTransformer
|
||||
{
|
||||
public function handle(Operation $operation, RouteInfo $routeInfo);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Contracts;
|
||||
|
||||
use Dedoc\Scramble\Support\Generator\Types\Type;
|
||||
use Dedoc\Scramble\Support\RuleTransforming\NormalizedRule;
|
||||
use Dedoc\Scramble\Support\RuleTransforming\RuleTransformerContext;
|
||||
|
||||
interface RuleTransformer
|
||||
{
|
||||
public function shouldHandle(NormalizedRule $rule): bool;
|
||||
|
||||
public function toSchema(Type $previous, NormalizedRule $rule, RuleTransformerContext $context): Type;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Contracts;
|
||||
|
||||
use Dedoc\Scramble\Configuration\SecurityDocumentationContext;
|
||||
use Dedoc\Scramble\GeneratorConfig;
|
||||
|
||||
interface SecurityDocumentationStrategy
|
||||
{
|
||||
public function configure(SecurityDocumentationContext $context): GeneratorConfig;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\DocumentTransformers;
|
||||
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Dedoc\Scramble\Contracts\DocumentTransformer;
|
||||
use Dedoc\Scramble\OpenApiContext;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\Tag;
|
||||
use Illuminate\Support\Collection;
|
||||
use ReflectionAttribute;
|
||||
|
||||
class AddDocumentTags implements DocumentTransformer
|
||||
{
|
||||
public function handle(OpenApi $document, OpenApiContext $context): void
|
||||
{
|
||||
$document->tags = $this->makeTagsFromGroupAttributes($context->groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, ReflectionAttribute<Group>> $groupsAttributes
|
||||
* @return Tag[]
|
||||
*/
|
||||
private function makeTagsFromGroupAttributes(Collection $groupsAttributes)
|
||||
{
|
||||
/** @var Collection<string, Tag> $tags */
|
||||
$tags = $groupsAttributes->reduce(function (Collection $acc, ReflectionAttribute $attribute) {
|
||||
$arguments = $attribute->getArguments();
|
||||
|
||||
$name = $arguments['name'] ?? $arguments[0] ?? null;
|
||||
|
||||
if (! $name) {
|
||||
return $acc;
|
||||
}
|
||||
|
||||
$description = $arguments['description'] ?? $arguments[1] ?? null;
|
||||
$weight = $arguments['weight'] ?? $arguments[2] ?? null;
|
||||
|
||||
/** @var Tag $tag */
|
||||
$tag = $acc->get($name, new Tag($name));
|
||||
|
||||
if ($description !== null && $tag->description === null) {
|
||||
$tag->description = $description;
|
||||
}
|
||||
|
||||
if ($weight !== null && $tag->getAttribute('weight') === null) {
|
||||
$tag->setAttribute('weight', $weight);
|
||||
}
|
||||
|
||||
$acc->offsetSet($name, $tag);
|
||||
|
||||
return $acc;
|
||||
}, collect());
|
||||
|
||||
return $tags->sortBy(fn (Tag $t) => $t->getAttribute('weight', INF))->values()->all();
|
||||
}
|
||||
}
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\DocumentTransformers;
|
||||
|
||||
use Dedoc\Scramble\Contracts\DocumentTransformer;
|
||||
use Dedoc\Scramble\OpenApiContext;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CleanupUnusedResponseReferencesTransformer implements DocumentTransformer
|
||||
{
|
||||
private string $serializedDocumentJson;
|
||||
|
||||
public function handle(OpenApi $document, OpenApiContext $context): void
|
||||
{
|
||||
$components = $document->components;
|
||||
$responses = $components->responses;
|
||||
|
||||
foreach ($responses as $responseName => $reference) {
|
||||
if (! $this->isResponseReferenceUsed($document, $context->references->responses->uniqueName($responseName))) {
|
||||
$components->removeResponse($responseName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function isResponseReferenceUsed(OpenApi $document, string $responseName): bool
|
||||
{
|
||||
if (! isset($this->serializedDocumentJson)) {
|
||||
$this->serializedDocumentJson = json_encode($document->toArray(), JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
$referencePath = "#/components/responses/{$responseName}";
|
||||
|
||||
return Str::contains($this->serializedDocumentJson, $referencePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Enums;
|
||||
|
||||
enum JsonApiArraySerialization
|
||||
{
|
||||
case String;
|
||||
case Comma;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Exceptions;
|
||||
|
||||
use Illuminate\Console\OutputStyle;
|
||||
|
||||
interface ConsoleRenderable
|
||||
{
|
||||
public function renderInConsole(OutputStyle $outputStyle): void;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Exceptions;
|
||||
|
||||
use Dedoc\Scramble\Console\Commands\Components\Code;
|
||||
use Dedoc\Scramble\Support\Generator\Types\Type;
|
||||
use Exception;
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Illuminate\Routing\Route;
|
||||
|
||||
class InvalidSchema extends Exception implements ConsoleRenderable, RouteAware
|
||||
{
|
||||
use RouteAwareTrait;
|
||||
|
||||
public ?string $jsonPointer = null;
|
||||
|
||||
public string $originalMessage = '';
|
||||
|
||||
public ?string $originFile = null;
|
||||
|
||||
public ?int $originLine = null;
|
||||
|
||||
public static function createForSchema(string $message, string $path, Type $schema)
|
||||
{
|
||||
/** @var string|null $file */
|
||||
$file = $schema->getAttribute('file');
|
||||
/** @var int|null $line */
|
||||
$line = $schema->getAttribute('line');
|
||||
|
||||
$originalMessage = $message;
|
||||
if ($file) {
|
||||
$message = rtrim($message, '.').'. Got when analyzing an expression in file ['.$file.'] on line '.$line;
|
||||
}
|
||||
|
||||
$exception = new static($path.': '.$message);
|
||||
|
||||
$exception->originalMessage = $originalMessage;
|
||||
$exception->originFile = $file;
|
||||
$exception->originLine = $line;
|
||||
$exception->jsonPointer = $path;
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
public function getRouteAwareMessage(Route $route, string $msg): string
|
||||
{
|
||||
$method = $route->methods()[0];
|
||||
$action = $route->getAction('uses');
|
||||
|
||||
return "'$method $route->uri' ($action): ".$msg;
|
||||
}
|
||||
|
||||
public function renderInConsole(OutputStyle $outputStyle): void
|
||||
{
|
||||
$codeSample = null;
|
||||
$tableCells = [];
|
||||
if ($this->jsonPointer) {
|
||||
$tableCells[] = ['<fg=gray>Found at</>', $this->jsonPointer];
|
||||
}
|
||||
|
||||
if ($this->originFile) {
|
||||
$line = $this->originLine ? ":$this->originLine" : '';
|
||||
|
||||
$tableCells[] = ['<fg=gray>Inferred at</>', "{$this->originFile}{$line}"];
|
||||
|
||||
if ($line) {
|
||||
$path = class_exists($this->originFile)
|
||||
? (new \ReflectionClass($this->originFile))->getFileName()
|
||||
: $this->originFile;
|
||||
|
||||
$codeSample = new Code($path, $this->originLine);
|
||||
}
|
||||
}
|
||||
|
||||
$outputStyle->createTable()->setRows($tableCells)->setStyle('compact')->render();
|
||||
|
||||
$codeSample?->render($outputStyle);
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Exceptions;
|
||||
|
||||
class OpenApiReferenceTargetNotFoundException extends \Exception {}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Exceptions;
|
||||
|
||||
use Illuminate\Routing\Route;
|
||||
|
||||
interface RouteAware
|
||||
{
|
||||
public function setRoute(Route $route): static;
|
||||
|
||||
public function getRoute(): ?Route;
|
||||
|
||||
public function getRouteAwareMessage(Route $route, string $msg): string;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Routing\Route;
|
||||
|
||||
/**
|
||||
* @mixin Exception
|
||||
*/
|
||||
trait RouteAwareTrait
|
||||
{
|
||||
protected ?Route $route = null;
|
||||
|
||||
public function setRoute(Route $route): static
|
||||
{
|
||||
$this->route = $route;
|
||||
|
||||
if (method_exists($this, 'getRouteAwareMessage')) {
|
||||
$this->message = $this->getRouteAwareMessage($route, $this->getMessage());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRoute(): ?Route
|
||||
{
|
||||
return $this->route;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Throwable;
|
||||
|
||||
class RulesEvaluationException extends Exception
|
||||
{
|
||||
/** @var array<string, Throwable> */
|
||||
public array $exceptions = [];
|
||||
|
||||
/**
|
||||
* @param array<string, Throwable> $exceptions
|
||||
*/
|
||||
public static function fromExceptions(array $exceptions): self
|
||||
{
|
||||
$exceptions = collect(Arr::wrap($exceptions))
|
||||
->reduce(function (array $acc, Throwable $e, string $key) {
|
||||
if ($e instanceof self) {
|
||||
return array_merge($acc, $e->exceptions);
|
||||
}
|
||||
|
||||
return [...$acc, $key => $e];
|
||||
}, []);
|
||||
|
||||
$previous = reset($exceptions) ?: null;
|
||||
|
||||
$exception = new self(self::buildMessage($exceptions), previous: $previous);
|
||||
$exception->exceptions = $exceptions;
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Throwable> $exceptions
|
||||
*/
|
||||
private static function buildMessage(array $exceptions): string
|
||||
{
|
||||
$lines = [
|
||||
'Cannot evaluate validation rules ('.count($exceptions).' evaluators failed):',
|
||||
];
|
||||
|
||||
foreach ($exceptions as $key => $e) {
|
||||
$name = class_basename($key);
|
||||
|
||||
$context = self::extractFileLocation($e);
|
||||
|
||||
$lines[] = " [$name] {$e->getMessage()}".($context ? " {$context}" : '');
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private static function extractFileLocation(Throwable $e): ?string
|
||||
{
|
||||
if ($e->getFile() && $e->getLine()) {
|
||||
return "(at {$e->getFile()}:{$e->getLine()})";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Extensions;
|
||||
|
||||
use Dedoc\Scramble\Infer;
|
||||
use Dedoc\Scramble\Support\Generator\Components;
|
||||
use Dedoc\Scramble\Support\Generator\TypeTransformer;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
|
||||
abstract class ExceptionToResponseExtension
|
||||
{
|
||||
protected Infer $infer;
|
||||
|
||||
protected TypeTransformer $openApiTransformer;
|
||||
|
||||
protected Components $components;
|
||||
|
||||
public function __construct(Infer $infer, TypeTransformer $openApiTransformer, Components $components)
|
||||
{
|
||||
$this->infer = $infer;
|
||||
$this->openApiTransformer = $openApiTransformer;
|
||||
$this->components = $components;
|
||||
}
|
||||
|
||||
public function toResponse(Type $type)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Extensions;
|
||||
|
||||
use Dedoc\Scramble\Contracts\OperationTransformer;
|
||||
use Dedoc\Scramble\GeneratorConfig;
|
||||
use Dedoc\Scramble\Infer;
|
||||
use Dedoc\Scramble\Support\Generator\Operation;
|
||||
use Dedoc\Scramble\Support\Generator\TypeTransformer;
|
||||
use Dedoc\Scramble\Support\RouteInfo;
|
||||
|
||||
abstract class OperationExtension implements OperationTransformer
|
||||
{
|
||||
public function __construct(
|
||||
protected Infer $infer,
|
||||
protected TypeTransformer $openApiTransformer,
|
||||
protected GeneratorConfig $config
|
||||
) {}
|
||||
|
||||
abstract public function handle(Operation $operation, RouteInfo $routeInfo);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Extensions;
|
||||
|
||||
use Dedoc\Scramble\Infer;
|
||||
use Dedoc\Scramble\Support\Generator\Components;
|
||||
use Dedoc\Scramble\Support\Generator\Response;
|
||||
use Dedoc\Scramble\Support\Generator\Types\Type as OpenApiType;
|
||||
use Dedoc\Scramble\Support\Generator\Types\UnknownType;
|
||||
use Dedoc\Scramble\Support\Generator\TypeTransformer;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
|
||||
abstract class TypeToSchemaExtension
|
||||
{
|
||||
public function __construct(protected Infer $infer, protected TypeTransformer $openApiTransformer, protected Components $components) {}
|
||||
|
||||
/**
|
||||
* @param Type $type The type being transformed to schema.
|
||||
* @param ?OpenApiType $previousExtensionResult The resulting schema from a previous extension.
|
||||
*/
|
||||
public function toSchema(Type $type)
|
||||
{
|
||||
return new UnknownType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $type The type being transformed to response.
|
||||
* @param ?Response $previousExtensionResult The resulting response from a previous extension.
|
||||
*/
|
||||
public function toResponse(Type $type)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble;
|
||||
|
||||
use Closure;
|
||||
use Dedoc\Scramble\Attributes\ExcludeAllRoutesFromDocs;
|
||||
use Dedoc\Scramble\Attributes\ExcludeRouteFromDocs;
|
||||
use Dedoc\Scramble\Configuration\SecurityDocumentationContext;
|
||||
use Dedoc\Scramble\Contracts\DocumentTransformer;
|
||||
use Dedoc\Scramble\Exceptions\RouteAware;
|
||||
use Dedoc\Scramble\OpenApiVisitor\SchemaEnforceVisitor;
|
||||
use Dedoc\Scramble\Support\ContainerUtils;
|
||||
use Dedoc\Scramble\Support\Generator\Components;
|
||||
use Dedoc\Scramble\Support\Generator\InfoObject;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\Operation;
|
||||
use Dedoc\Scramble\Support\Generator\Path;
|
||||
use Dedoc\Scramble\Support\Generator\Reference;
|
||||
use Dedoc\Scramble\Support\Generator\Server;
|
||||
use Dedoc\Scramble\Support\Generator\TypeTransformer;
|
||||
use Dedoc\Scramble\Support\Generator\UniqueNameOptions;
|
||||
use Dedoc\Scramble\Support\Generator\UniqueNamesOptionsCollection;
|
||||
use Dedoc\Scramble\Support\OperationBuilder;
|
||||
use Dedoc\Scramble\Support\RouteInfo;
|
||||
use Dedoc\Scramble\Support\ServerFactory;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use Throwable;
|
||||
|
||||
class Generator
|
||||
{
|
||||
public array $exceptions = [];
|
||||
|
||||
protected bool $throwExceptions = true;
|
||||
|
||||
public function __construct(
|
||||
private OperationBuilder $operationBuilder,
|
||||
) {}
|
||||
|
||||
public function setThrowExceptions(bool $throwExceptions): static
|
||||
{
|
||||
$this->throwExceptions = $throwExceptions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __invoke(?GeneratorConfig $config = null)
|
||||
{
|
||||
$config ??= Scramble::getGeneratorConfig(Scramble::DEFAULT_API);
|
||||
|
||||
$routes = $this->getRoutes($config);
|
||||
$config = $this->configureSecurityStrategy($routes, $config);
|
||||
|
||||
$openApi = $this->makeOpenApi($config);
|
||||
$context = new OpenApiContext($openApi, $config);
|
||||
|
||||
$typeTransformer = $this->buildTypeTransformer($context);
|
||||
|
||||
$routes
|
||||
->flatMap(function (Route $route, int $index) use ($openApi, $config, $typeTransformer) {
|
||||
try {
|
||||
$operations = $this->routeToOperations($openApi, $route, $config, $typeTransformer);
|
||||
|
||||
foreach ($operations as $i => $operation) {
|
||||
if ($route->getAction('uses') instanceof Closure) {
|
||||
$operation->setAttribute('isClosure', true);
|
||||
}
|
||||
|
||||
$operation->setAttribute('index', $index + $i);
|
||||
}
|
||||
|
||||
return $operations;
|
||||
} catch (Throwable $e) {
|
||||
if ($e instanceof RouteAware) {
|
||||
$e->setRoute($route);
|
||||
}
|
||||
|
||||
if (config('app.debug', false)) {
|
||||
$method = implode('|', $route->methods());
|
||||
$action = $route->getAction('uses');
|
||||
if ($action instanceof Closure) {
|
||||
$action = '{closure}';
|
||||
}
|
||||
|
||||
dump("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – ".($e->getFile().' on line '.$e->getLine()));
|
||||
logger()->error("Error when analyzing route '$method $route->uri' ($action): {$e->getMessage()} – ".($e->getFile().' on line '.$e->getLine()));
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
})
|
||||
->filter()
|
||||
->sortBy($this->createOperationsSorter())
|
||||
->each(fn (Operation $operation) => $openApi->addPath(
|
||||
Path::make(
|
||||
$config->apiPath()->stripPrefix($operation->path)
|
||||
)->addOperation($operation)
|
||||
))
|
||||
->toArray();
|
||||
|
||||
$this->setUniqueOperationId($openApi);
|
||||
|
||||
$this->moveSameAlternativeServersToPath($openApi);
|
||||
|
||||
foreach ($config->documentTransformers->all() as $openApiTransformer) {
|
||||
$openApiTransformer = is_callable($openApiTransformer)
|
||||
? $openApiTransformer
|
||||
: ContainerUtils::makeContextable($openApiTransformer, [
|
||||
TypeTransformer::class => $typeTransformer,
|
||||
]);
|
||||
|
||||
if (is_callable($openApiTransformer)) {
|
||||
$openApiTransformer($openApi, $context);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($openApiTransformer instanceof DocumentTransformer) {
|
||||
$openApiTransformer->handle($openApi, $context);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// @phpstan-ignore deadCode.unreachable
|
||||
throw new InvalidArgumentException('(callable(OpenApi, OpenApiContext): void)|DocumentTransformer type for document transformer expected, received '.$openApiTransformer::class);
|
||||
}
|
||||
|
||||
return $openApi->toArray();
|
||||
}
|
||||
|
||||
private function configureSecurityStrategy(Collection $routes, GeneratorConfig $config): GeneratorConfig
|
||||
{
|
||||
$strategy = $config->securityStrategy();
|
||||
|
||||
if (! $strategy) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
return $strategy->configure(new SecurityDocumentationContext($routes, $config->cloneWithoutExposing()));
|
||||
}
|
||||
|
||||
private function createOperationsSorter(): array
|
||||
{
|
||||
$defaultSortValue = fn (Operation $o) => $o->tags[0] ?? null;
|
||||
|
||||
return [
|
||||
fn (Operation $a, Operation $b) => $a->getAttribute('groupWeight', INF) <=> $b->getAttribute('groupWeight', INF),
|
||||
fn (Operation $a, Operation $b) => $a->getAttribute('weight', INF) <=> $b->getAttribute('weight', INF), // @todo manual endpoint sorting
|
||||
fn (Operation $a, Operation $b) => $defaultSortValue($a) <=> $defaultSortValue($b),
|
||||
fn (Operation $a, Operation $b) => $a->getAttribute('index', INF) <=> $b->getAttribute('index', INF),
|
||||
];
|
||||
}
|
||||
|
||||
private function makeOpenApi(GeneratorConfig $config)
|
||||
{
|
||||
$openApi = OpenApi::make('3.1.0')
|
||||
->setComponents(new Components)
|
||||
->setInfo(
|
||||
InfoObject::make($config->get('ui.title', $default = config('app.name')) ?: $default)
|
||||
->setVersion($config->get('info.version', '0.0.1'))
|
||||
->setDescription($config->get('info.description', ''))
|
||||
);
|
||||
|
||||
[$defaultProtocol] = explode('://', url('/'));
|
||||
$servers = $config->get('servers') ?: [
|
||||
'' => ($domain = $config->get('api_domain'))
|
||||
? $defaultProtocol.'://'.$domain.'/'.$config->apiPath()->serverPath()
|
||||
: $config->apiPath()->serverPath(),
|
||||
];
|
||||
foreach ($servers as $description => $url) {
|
||||
$openApi->addServer(
|
||||
(new ServerFactory($config->serverVariables->all()))->make(url($url ?: '/'), $description)
|
||||
);
|
||||
}
|
||||
|
||||
return $openApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Route>
|
||||
*/
|
||||
private function getRoutes(GeneratorConfig $config): Collection
|
||||
{
|
||||
return collect(RouteFacade::getRoutes())
|
||||
->pipe(function (Collection $c) {
|
||||
$onlyRoutes = $c->filter(function (Route $route) {
|
||||
|
||||
if (! is_string($route->getAction('controller'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! is_string($route->getAction('uses'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new ReflectionMethod(...explode('@', $route->getAction('uses')));
|
||||
|
||||
if (str_contains($reflection->getDocComment() ?: '', '@only-docs')) {
|
||||
return true;
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return $onlyRoutes->count() ? $onlyRoutes : $c;
|
||||
})
|
||||
->filter(function (Route $route) {
|
||||
return ! ($name = $route->getAction('as')) || ! Str::startsWith($name, 'scramble');
|
||||
})
|
||||
->filter($config->routes())
|
||||
->filter(function (Route $route) {
|
||||
if (! is_string($route->getAction('uses'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = new ReflectionMethod(...explode('@', $route->getAction('uses')));
|
||||
} catch (ReflectionException) {
|
||||
/*
|
||||
* If route is registered but route method doesn't exist, it will not be included
|
||||
* in the resulting documentation.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($reflection->getAttributes(ExcludeRouteFromDocs::class))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($reflection->getDeclaringClass()->getAttributes(ExcludeAllRoutesFromDocs::class))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
private function buildTypeTransformer(OpenApiContext $context): TypeTransformer
|
||||
{
|
||||
return app()->make(TypeTransformer::class, [
|
||||
'context' => $context,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Operation[] */
|
||||
private function routeToOperations(OpenApi $openApi, Route $route, GeneratorConfig $config, TypeTransformer $typeTransformer): array
|
||||
{
|
||||
$methods = array_map('strtolower', Arr::wrap(($config->operationMethodsResolver)($route)));
|
||||
|
||||
$operations = [];
|
||||
foreach ($methods as $method) {
|
||||
$routeInfo = new RouteInfo($route, $method);
|
||||
|
||||
$operation = $this->operationBuilder->build($routeInfo, $openApi, $config, $typeTransformer);
|
||||
|
||||
$this->ensureSchemaTypes($route, $operation);
|
||||
|
||||
$operations[] = $operation;
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
private function ensureSchemaTypes(Route $route, Operation $operation): void
|
||||
{
|
||||
if (! Scramble::getSchemaValidator()->hasRules()) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$traverser, $visitor] = $this->createSchemaEnforceTraverser($route);
|
||||
|
||||
$traverser->traverse($operation, ['', 'paths', $operation->path, $operation->method]);
|
||||
$references = $visitor->popReferences();
|
||||
|
||||
/** @var Reference $ref */
|
||||
foreach ($references as $ref) {
|
||||
if ($resolvedType = $ref->resolve()) {
|
||||
$traverser->traverse($resolvedType, ['', 'components', $ref->referenceType, $ref->getUniqueName()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createSchemaEnforceTraverser(Route $route)
|
||||
{
|
||||
$traverser = new OpenApiTraverser([$visitor = new SchemaEnforceVisitor($route, $this->throwExceptions, $this->exceptions)]);
|
||||
|
||||
return [$traverser, $visitor];
|
||||
}
|
||||
|
||||
private function moveSameAlternativeServersToPath(OpenApi $openApi)
|
||||
{
|
||||
foreach (collect($openApi->paths)->groupBy('path') as $pathsGroup) {
|
||||
if ($pathsGroup->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$operations = collect($pathsGroup->pluck('operations')->flatten());
|
||||
|
||||
$operationsHaveSameAlternativeServers = $operations->count()
|
||||
&& $operations->every(fn (Operation $o) => count($o->servers))
|
||||
&& $operations->unique(function (Operation $o) {
|
||||
return collect($o->servers)->map(fn (Server $s) => $s->url)->join('.');
|
||||
})->count() === 1;
|
||||
|
||||
if (! $operationsHaveSameAlternativeServers) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pathsGroup->every->servers($operations->first()->servers);
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$operation->servers([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setUniqueOperationId(OpenApi $openApi)
|
||||
{
|
||||
$names = new UniqueNamesOptionsCollection;
|
||||
|
||||
$this->foreachOperation($openApi, function (Operation $operation) use ($names) {
|
||||
if ($operation->operationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$names->push($operation->getAttribute('operationId')); // @phpstan-ignore argument.type
|
||||
});
|
||||
|
||||
$this->foreachOperation($openApi, function (Operation $operation, $index) use ($names) {
|
||||
if ($operation->operationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $operation->getAttribute('operationId');
|
||||
if (! $name instanceof UniqueNameOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $name->eloquent && $operation->getAttribute('isClosure')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$operation->setOperationId($names->getUniqueName($name, function (string $fallback) use ($index) {
|
||||
return "{$fallback}_{$index}";
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private function foreachOperation(OpenApi $openApi, callable $callback)
|
||||
{
|
||||
foreach (collect($openApi->paths)->groupBy('path') as $pathsGroup) {
|
||||
if ($pathsGroup->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$operations = collect($pathsGroup->pluck('operations')->flatten());
|
||||
|
||||
foreach ($operations as $index => $operation) {
|
||||
$callback($operation, $index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble;
|
||||
|
||||
use Closure;
|
||||
use Dedoc\Scramble\Configuration\ApiPath;
|
||||
use Dedoc\Scramble\Configuration\DocumentTransformers;
|
||||
use Dedoc\Scramble\Configuration\JsonApiConfig;
|
||||
use Dedoc\Scramble\Configuration\OperationTransformers;
|
||||
use Dedoc\Scramble\Configuration\ParametersExtractors;
|
||||
use Dedoc\Scramble\Configuration\RendererConfig;
|
||||
use Dedoc\Scramble\Configuration\RuleTransformers;
|
||||
use Dedoc\Scramble\Configuration\ServerVariables;
|
||||
use Dedoc\Scramble\Contracts\AllRulesSchemasTransformer;
|
||||
use Dedoc\Scramble\Contracts\RuleTransformer;
|
||||
use Dedoc\Scramble\Contracts\SecurityDocumentationStrategy;
|
||||
use Dedoc\Scramble\Enums\JsonApiArraySerialization;
|
||||
use Dedoc\Scramble\Support\Generator\ServerVariable;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Arr;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionNamedType;
|
||||
|
||||
class GeneratorConfig
|
||||
{
|
||||
/**
|
||||
* @var (Closure(Router, mixed): Route)|string|null
|
||||
*/
|
||||
public Closure|string|null $uiRoute = null;
|
||||
|
||||
/**
|
||||
* @var (Closure(Router, mixed): Route)|string|null
|
||||
*/
|
||||
public Closure|string|null $documentRoute = null;
|
||||
|
||||
/**
|
||||
* @var Closure(Route): (string|string[])
|
||||
*/
|
||||
public Closure $operationMethodsResolver;
|
||||
|
||||
public function __construct(
|
||||
private array $config = [],
|
||||
private ?Closure $routeResolver = null,
|
||||
public readonly ParametersExtractors $parametersExtractors = new ParametersExtractors,
|
||||
public readonly OperationTransformers $operationTransformers = new OperationTransformers,
|
||||
public readonly DocumentTransformers $documentTransformers = new DocumentTransformers,
|
||||
public readonly RuleTransformers $ruleTransformers = new RuleTransformers,
|
||||
public readonly ServerVariables $serverVariables = new ServerVariables,
|
||||
?Closure $operationMethodsResolver = null,
|
||||
public JsonApiConfig $jsonApi = new JsonApiConfig,
|
||||
private ?ApiPath $resolvedApiPath = null,
|
||||
) {
|
||||
$this->operationMethodsResolver = $operationMethodsResolver ?: fn (Route $r) => $r->methods()[0];
|
||||
}
|
||||
|
||||
public function config(array $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->resolvedApiPath = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function routes(?Closure $routeResolver = null)
|
||||
{
|
||||
if (count(func_get_args()) === 0) {
|
||||
return $this->routeResolver ?: $this->defaultRoutesFilter(...);
|
||||
}
|
||||
|
||||
if ($routeResolver) {
|
||||
$this->routeResolver = $routeResolver;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function renderer(): RendererConfig
|
||||
{
|
||||
if (Arr::has($this->config, 'ui.logo')) {
|
||||
return new RendererConfig(array_merge(
|
||||
$this->get('renderers.elements', []),
|
||||
$this->get('ui'),
|
||||
));
|
||||
}
|
||||
|
||||
return new RendererConfig($this->get('renderers.'.$this->get('renderer'), []));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (Closure(Router, mixed): Route)|string|false $ui
|
||||
* @param (Closure(Router, mixed): Route)|string|false $document
|
||||
*/
|
||||
public function expose(Closure|string|false $ui = false, Closure|string|false $document = false): static
|
||||
{
|
||||
if (count(func_get_args()) === 1 && isset(func_get_args()[0]) && func_get_args()[0] === false) {
|
||||
$this->uiRoute = null;
|
||||
$this->documentRoute = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->uiRoute = $ui ?: null;
|
||||
$this->documentRoute = $document ?: null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function defaultRoutesFilter(Route $route)
|
||||
{
|
||||
$expectedDomain = $this->get('api_domain');
|
||||
|
||||
return $this->apiPath()->matches($route->uri)
|
||||
&& (! $expectedDomain || $route->getDomain() === $expectedDomain);
|
||||
}
|
||||
|
||||
public function apiPath(): ApiPath
|
||||
{
|
||||
return $this->resolvedApiPath ??= ApiPath::from($this->get('api_path'), 'api');
|
||||
}
|
||||
|
||||
public function afterOpenApiGenerated(?callable $afterOpenApiGenerated = null)
|
||||
{
|
||||
if (count(func_get_args()) === 0) {
|
||||
return $this->documentTransformers->all();
|
||||
}
|
||||
|
||||
if ($afterOpenApiGenerated) {
|
||||
$this->documentTransformers->append($afterOpenApiGenerated);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function useConfig(array $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->resolvedApiPath = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function cloneWithoutExposing(): static
|
||||
{
|
||||
return new GeneratorConfig(
|
||||
config: $this->config,
|
||||
routeResolver: $this->routeResolver,
|
||||
parametersExtractors: clone $this->parametersExtractors,
|
||||
operationTransformers: clone $this->operationTransformers,
|
||||
documentTransformers: clone $this->documentTransformers,
|
||||
ruleTransformers: clone $this->ruleTransformers,
|
||||
serverVariables: clone $this->serverVariables,
|
||||
operationMethodsResolver: $this->operationMethodsResolver,
|
||||
jsonApi: clone $this->jsonApi,
|
||||
);
|
||||
}
|
||||
|
||||
public function withParametersExtractors(callable $callback): static
|
||||
{
|
||||
$callback($this->parametersExtractors);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withOperationTransformers(array|string|callable $cb): static
|
||||
{
|
||||
if ($this->isOperationTransformerMapper($cb)) {
|
||||
$cb($this->operationTransformers);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->operationTransformers->append($cb);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<class-string<RuleTransformer|AllRulesSchemasTransformer>>|class-string<RuleTransformer|AllRulesSchemasTransformer>|(callable(RuleTransformers): void) $cb
|
||||
*/
|
||||
public function withRuleTransformers(array|string|callable $cb): self
|
||||
{
|
||||
if (is_callable($cb)) {
|
||||
$cb($this->ruleTransformers);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->ruleTransformers->append($cb);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function isOperationTransformerMapper($cb): bool
|
||||
{
|
||||
if (! $cb instanceof Closure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionFunction($cb);
|
||||
|
||||
return count($reflection->getParameters()) === 1
|
||||
&& $reflection->getParameters()[0]->getType() instanceof ReflectionNamedType
|
||||
&& is_a($reflection->getParameters()[0]->getType()->getName(), OperationTransformers::class, true);
|
||||
}
|
||||
|
||||
public function withDocumentTransformers(array|string|callable $cb): static
|
||||
{
|
||||
if ($this->isDocumentTransformerMapper($cb)) {
|
||||
$cb($this->documentTransformers);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->documentTransformers->append($cb);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function isDocumentTransformerMapper($cb): bool
|
||||
{
|
||||
if (! $cb instanceof Closure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionFunction($cb);
|
||||
|
||||
return count($reflection->getParameters()) === 1
|
||||
&& $reflection->getParameters()[0]->getType() instanceof ReflectionNamedType
|
||||
&& is_a($reflection->getParameters()[0]->getType()->getName(), DocumentTransformers::class, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (callable(ServerVariables): void)|array<string, ServerVariable> $variables
|
||||
*/
|
||||
public function withServerVariables(callable|array $variables)
|
||||
{
|
||||
if (is_callable($variables)) {
|
||||
$variables($this->serverVariables);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->serverVariables->use($variables);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force Scramble to use `PATCH` for `PUT|PATCH` routes.
|
||||
*/
|
||||
public function preferPatchMethod(): static
|
||||
{
|
||||
return $this->resolveOperationMethodsUsing(function (Route $route): string {
|
||||
$methods = array_map('strtolower', $route->methods());
|
||||
|
||||
return in_array('patch', $methods) && in_array('put', $methods) ? 'patch' : $methods[0];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(Route): (string|string[]) $resolver
|
||||
*/
|
||||
public function resolveOperationMethodsUsing(Closure $resolver): static
|
||||
{
|
||||
$this->operationMethodsResolver = $resolver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function jsonApi(
|
||||
JsonApiArraySerialization $arraySerialization = JsonApiArraySerialization::Comma,
|
||||
?int $maxRelationshipDepth = null,
|
||||
): static {
|
||||
$this->jsonApi = new JsonApiConfig(
|
||||
arraySerialization: $arraySerialization,
|
||||
maxRelationshipDepth: $maxRelationshipDepth,
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null)
|
||||
{
|
||||
return Arr::get($this->config, $key, $default);
|
||||
}
|
||||
|
||||
public function securityStrategy(): ?SecurityDocumentationStrategy
|
||||
{
|
||||
$value = $this->get('security_strategy');
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$class, $options] = is_string($value)
|
||||
? [$value, []]
|
||||
: (is_array($value) && count($value) === 2 && is_string($value[0])
|
||||
? [$value[0], $value[1]]
|
||||
: [null, []]);
|
||||
|
||||
if ($class === null) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid scramble.security_strategy config. Expected null, a class-string, or [class-string, options array].'
|
||||
);
|
||||
}
|
||||
|
||||
$strategy = app($class, $options);
|
||||
|
||||
if (! $strategy instanceof SecurityDocumentationStrategy) {
|
||||
throw new InvalidArgumentException(
|
||||
"Security strategy [{$class}] must implement ".SecurityDocumentationStrategy::class.'.'
|
||||
);
|
||||
}
|
||||
|
||||
return $strategy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Http\Middleware;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class RestrictedDocsAccess
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
if (app()->environment('local')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (Gate::allows('viewApiDocs')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble;
|
||||
|
||||
use Dedoc\Scramble\Configuration\InferConfig;
|
||||
use Dedoc\Scramble\Infer\Analyzer\ClassAnalyzer;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
|
||||
use Dedoc\Scramble\Infer\Scope\Index;
|
||||
|
||||
class Infer
|
||||
{
|
||||
public function __construct(
|
||||
public Index $index,
|
||||
public InferConfig $config,
|
||||
) {}
|
||||
|
||||
public function analyzeClass(string $class): ClassDefinition
|
||||
{
|
||||
if (! $this->index->getClassDefinition($class)) {
|
||||
$this->index->registerClassDefinition(
|
||||
(new ClassAnalyzer($this->index))->analyze($class)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->index->getClassDefinition($class);
|
||||
}
|
||||
|
||||
public function configure(): InferConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Analyzer;
|
||||
|
||||
use Dedoc\Scramble\Infer\Context;
|
||||
use Dedoc\Scramble\Infer\Definition\AttributeDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassPropertyDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\PendingDocComment;
|
||||
use Dedoc\Scramble\Infer\Definition\PropertyVisibility;
|
||||
use Dedoc\Scramble\Infer\Extensions\Event\ClassDefinitionCreatedEvent;
|
||||
use Dedoc\Scramble\Infer\Scope\Index;
|
||||
use Dedoc\Scramble\Support\Type\TemplateType;
|
||||
use Dedoc\Scramble\Support\Type\TypeHelper;
|
||||
use Dedoc\Scramble\Support\Type\UnknownType;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionClass;
|
||||
|
||||
class ClassAnalyzer
|
||||
{
|
||||
public function __construct(private Index $index) {}
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function analyze(string $name): ClassDefinition
|
||||
{
|
||||
$classReflection = new ReflectionClass($name); // @phpstan-ignore argument.type
|
||||
|
||||
$parentName = ($classReflection->getParentClass() ?: null)?->name;
|
||||
|
||||
$parentDefinition = $parentName ? $this->index->getClass($parentName) : null;
|
||||
|
||||
$classDefinition = new ClassDefinition(
|
||||
name: $name,
|
||||
templateTypes: $parentDefinition?->templateTypes ?: [],
|
||||
properties: array_map(fn ($pd) => clone $pd, $parentDefinition?->properties ?: []),
|
||||
methods: array_map(fn ($md) => $md->copyFromParent(), $parentDefinition?->methods ?: []),
|
||||
parentFqn: $parentName,
|
||||
);
|
||||
|
||||
/*
|
||||
* Traits get analyzed by embracing default behavior of PHP reflection: reflection properties and
|
||||
* reflection methods get copied into the class that uses the trait.
|
||||
*/
|
||||
|
||||
foreach ($classReflection->getProperties() as $reflectionProperty) {
|
||||
if ($reflectionProperty->class !== $name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists($reflectionProperty->name, $classDefinition->properties)) {
|
||||
$classDefinition->properties[$reflectionProperty->name]->defaultType = $reflectionProperty->hasDefaultValue()
|
||||
? PropertyAnalyzer::from($reflectionProperty)->getDefaultType()
|
||||
: null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($reflectionProperty->isStatic()) {
|
||||
$classDefinition->properties[$reflectionProperty->name] = new ClassPropertyDefinition(
|
||||
type: $reflectionProperty->hasDefaultValue()
|
||||
? (TypeHelper::createTypeFromValue($reflectionProperty->getDefaultValue()) ?: new UnknownType)
|
||||
: new UnknownType,
|
||||
isStatic: $reflectionProperty->isStatic(),
|
||||
visibility: PropertyVisibility::fromReflectionProperty($reflectionProperty),
|
||||
attributes: AttributeDefinition::fromReflectionAttributesArray($reflectionProperty->getAttributes()),
|
||||
pendingDocComment: ($docComment = $reflectionProperty->getDocComment() ?: null)
|
||||
? new PendingDocComment($docComment, declaringClass: $name)
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
$expectedTemplateTypeName = 'T'.Str::studly($reflectionProperty->name);
|
||||
|
||||
$existingPropertyTemplateType = collect($classDefinition->templateTypes)
|
||||
->first(fn (TemplateType $t) => $t->name === $expectedTemplateTypeName);
|
||||
|
||||
$propertyTemplateType = $existingPropertyTemplateType ?: new TemplateType(
|
||||
$expectedTemplateTypeName,
|
||||
is: ($reflectionPropertyType = $reflectionProperty->getType()) ? TypeHelper::createTypeFromReflectionType($reflectionPropertyType) : new UnknownType,
|
||||
);
|
||||
|
||||
$classDefinition->properties[$reflectionProperty->name] = new ClassPropertyDefinition(
|
||||
type: $propertyTemplateType,
|
||||
defaultType: $reflectionProperty->hasDefaultValue()
|
||||
? PropertyAnalyzer::from($reflectionProperty)->getDefaultType()
|
||||
: null,
|
||||
visibility: PropertyVisibility::fromReflectionProperty($reflectionProperty),
|
||||
attributes: AttributeDefinition::fromReflectionAttributesArray($reflectionProperty->getAttributes()),
|
||||
pendingDocComment: ($docComment = $reflectionProperty->getDocComment() ?: null)
|
||||
? new PendingDocComment($docComment, declaringClass: $name)
|
||||
: null,
|
||||
);
|
||||
|
||||
if (! $existingPropertyTemplateType) {
|
||||
$classDefinition->templateTypes[] = $propertyTemplateType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$classDefinition->setIndex($this->index);
|
||||
|
||||
$this->index->registerClassDefinition($classDefinition);
|
||||
|
||||
Context::getInstance()->extensionsBroker->afterClassDefinitionCreated(new ClassDefinitionCreatedEvent($classDefinition->name, $classDefinition));
|
||||
|
||||
return $classDefinition;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Analyzer;
|
||||
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
use Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeAstDefinitionBuilder;
|
||||
use Dedoc\Scramble\Infer\Reflector\ClassReflector;
|
||||
use Dedoc\Scramble\Infer\Scope\Index;
|
||||
use Dedoc\Scramble\Infer\Services\FileNameResolver;
|
||||
|
||||
class MethodAnalyzer
|
||||
{
|
||||
public function __construct(
|
||||
private Index $index,
|
||||
private ClassDefinition $classDefinition,
|
||||
) {}
|
||||
|
||||
public function analyze(FunctionLikeDefinition $methodDefinition, array $indexBuilders = [], bool $withSideEffects = false)
|
||||
{
|
||||
try {
|
||||
$node = $this->getClassReflector()->getMethod($methodDefinition->type->name)->getAstNode();
|
||||
} catch (\ReflectionException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $node) {
|
||||
return $methodDefinition;
|
||||
}
|
||||
|
||||
return $this->classDefinition->methods[$methodDefinition->type->name] = (new FunctionLikeAstDefinitionBuilder(
|
||||
$methodDefinition->type->name,
|
||||
$node,
|
||||
$this->index,
|
||||
new FileNameResolver($this->getClassReflector()->getNameContext()),
|
||||
$this->classDefinition,
|
||||
$indexBuilders,
|
||||
$withSideEffects
|
||||
))->build();
|
||||
}
|
||||
|
||||
private function getClassReflector(): ClassReflector
|
||||
{
|
||||
return ClassReflector::make($this->classDefinition->name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Analyzer;
|
||||
|
||||
use Dedoc\Scramble\Infer\Context;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
|
||||
use Dedoc\Scramble\Infer\Reflector\PropertyReflector;
|
||||
use Dedoc\Scramble\Infer\Scope\Index;
|
||||
use Dedoc\Scramble\Infer\Scope\NodeTypesResolver;
|
||||
use Dedoc\Scramble\Infer\Scope\Scope;
|
||||
use Dedoc\Scramble\Infer\Scope\ScopeContext;
|
||||
use Dedoc\Scramble\Infer\Services\FileNameResolver;
|
||||
use Dedoc\Scramble\Infer\TypeInferer;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\TypeHelper;
|
||||
use Illuminate\Support\Arr;
|
||||
use PhpParser\Node\PropertyItem;
|
||||
use PhpParser\NodeTraverser;
|
||||
use ReflectionProperty;
|
||||
|
||||
class PropertyAnalyzer
|
||||
{
|
||||
public function __construct(private ReflectionProperty $reflectionProperty) {}
|
||||
|
||||
public static function from(ReflectionProperty $reflectionProperty): self
|
||||
{
|
||||
return new self($reflectionProperty);
|
||||
}
|
||||
|
||||
public function getDefaultType(): ?Type
|
||||
{
|
||||
if (! $this->reflectionProperty->hasDefaultValue()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($astType = $this->getDefaultTypeFromAst()) {
|
||||
return $astType;
|
||||
}
|
||||
|
||||
return TypeHelper::createTypeFromValue($this->reflectionProperty->getDefaultValue());
|
||||
}
|
||||
|
||||
private function getDefaultTypeFromAst(): ?Type
|
||||
{
|
||||
$reflector = PropertyReflector::make($this->reflectionProperty->getDeclaringClass()->name, $this->reflectionProperty->name);
|
||||
|
||||
$astNode = $reflector->getAstNode();
|
||||
if (! $astNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$propertyItem = collect($astNode->props)->first(fn (PropertyItem $p) => $p->name->name === $this->reflectionProperty->name);
|
||||
if (! $propertyItem instanceof PropertyItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$index = app(Index::class);
|
||||
$traverser = new NodeTraverser;
|
||||
$nameResolver = new FileNameResolver($reflector->getClassReflector()->getNameContext());
|
||||
$traverser->addVisitor($inferer = new TypeInferer(
|
||||
$index,
|
||||
$nameResolver,
|
||||
$scope = new Scope($index, new NodeTypesResolver, new ScopeContext(new ClassDefinition($this->reflectionProperty->getDeclaringClass()->name)), $nameResolver),
|
||||
Context::getInstance()->extensionsBroker->extensions,
|
||||
));
|
||||
$traverser->traverse(Arr::wrap($propertyItem));
|
||||
|
||||
if (! $propertyItem->default) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $scope->getType($propertyItem->default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer;
|
||||
|
||||
use Dedoc\Scramble\Infer\Contracts\ArgumentTypeBag;
|
||||
use Dedoc\Scramble\Infer\Scope\Scope;
|
||||
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\UnknownType;
|
||||
|
||||
class AutoResolvingArgumentTypeBag implements ArgumentTypeBag
|
||||
{
|
||||
/**
|
||||
* @param array<array-key, Type> $arguments
|
||||
*/
|
||||
public function __construct(private Scope $scope, private array $arguments) {}
|
||||
|
||||
public function get(string $name, int $position, ?Type $default = new UnknownType): ?Type
|
||||
{
|
||||
if ($argumentType = $this->arguments[$name] ?? $this->arguments[$position] ?? null) {
|
||||
return ReferenceTypeResolver::getInstance()->resolve($this->scope, $argumentType);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function map(callable $cb): ArgumentTypeBag
|
||||
{
|
||||
return new self(
|
||||
$this->scope,
|
||||
collect($this->arguments)->map(fn ($t, $key) => $cb($t, $key))->all(),
|
||||
);
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
return array_map(
|
||||
fn ($t) => ReferenceTypeResolver::getInstance()->resolve($this->scope, $t),
|
||||
$this->arguments,
|
||||
);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->arguments);
|
||||
}
|
||||
|
||||
/** @return array<array-key, Type> */
|
||||
public function allUnresolved(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Configuration;
|
||||
|
||||
class ClassLike implements DefinitionMatcher
|
||||
{
|
||||
public function __construct(public readonly string $class) {}
|
||||
|
||||
public function matches(string $class): bool
|
||||
{
|
||||
return $class === $this->class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Configuration;
|
||||
|
||||
class ClassLikeAndChildren implements DefinitionMatcher
|
||||
{
|
||||
public function __construct(public readonly string $class) {}
|
||||
|
||||
public function matches(string $class): bool
|
||||
{
|
||||
return is_a($class, $this->class, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Configuration;
|
||||
|
||||
interface DefinitionMatcher
|
||||
{
|
||||
public function matches(string $class): bool;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer;
|
||||
|
||||
use Dedoc\Scramble\Infer\Extensions\ExtensionsBroker;
|
||||
|
||||
class Context
|
||||
{
|
||||
private static $instance = null;
|
||||
|
||||
public function __construct(
|
||||
public readonly ExtensionsBroker $extensionsBroker,
|
||||
) {}
|
||||
|
||||
public static function configure(
|
||||
ExtensionsBroker $extensionsBroker,
|
||||
) {
|
||||
static::$instance = new static(
|
||||
$extensionsBroker,
|
||||
);
|
||||
}
|
||||
|
||||
public static function getInstance(): static
|
||||
{
|
||||
if (! static::$instance) {
|
||||
static::$instance = new static(
|
||||
app(ExtensionsBroker::class),
|
||||
);
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
public static function reset()
|
||||
{
|
||||
static::$instance = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Contracts;
|
||||
|
||||
use Countable;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\UnknownType;
|
||||
|
||||
interface ArgumentTypeBag extends Countable
|
||||
{
|
||||
public function get(string $name, int $position, ?Type $default = new UnknownType): ?Type;
|
||||
|
||||
/** @return array<array-key, Type> */
|
||||
public function all(): array;
|
||||
|
||||
/**
|
||||
* @param callable(Type, string|int): Type $cb
|
||||
*/
|
||||
public function map(callable $cb): self;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Contracts;
|
||||
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition as ClassDefinitionData;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
|
||||
interface ClassDefinition
|
||||
{
|
||||
public function getMethod(string $name): ?FunctionLikeDefinition;
|
||||
|
||||
public function getData(): ClassDefinitionData;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Contracts;
|
||||
|
||||
interface ClassDefinitionBuilder
|
||||
{
|
||||
public function build(): ClassDefinition;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Contracts;
|
||||
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
|
||||
interface FunctionLikeDefinitionBuilder
|
||||
{
|
||||
public function build(): FunctionLikeDefinition;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Contracts;
|
||||
|
||||
use Dedoc\Scramble\Infer\Contracts\ClassDefinition as ClassDefinitionContract;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
|
||||
interface Index
|
||||
{
|
||||
public function getFunction(string $name): ?FunctionLikeDefinition;
|
||||
|
||||
public function getClass(string $name): ?ClassDefinitionContract;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use ReflectionAttribute;
|
||||
|
||||
class AttributeDefinition
|
||||
{
|
||||
public const IS_INSTANCEOF = ReflectionAttribute::IS_INSTANCEOF;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public array $arguments = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param ReflectionAttribute[] $reflectionAttributes
|
||||
* @return self[]
|
||||
*/
|
||||
public static function fromReflectionAttributesArray(array $reflectionAttributes): array
|
||||
{
|
||||
return array_map(static::fromReflectionAttribute(...), $reflectionAttributes);
|
||||
}
|
||||
|
||||
public static function fromReflectionAttribute(ReflectionAttribute $reflectionAttribute): self
|
||||
{
|
||||
return new self(
|
||||
name: $reflectionAttribute->getName(),
|
||||
arguments: $reflectionAttribute->getArguments(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use Dedoc\Scramble\Infer\Analyzer\MethodAnalyzer;
|
||||
use Dedoc\Scramble\Infer\Contracts\ClassDefinition as ClassDefinitionContract;
|
||||
use Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeAstDefinitionBuilder;
|
||||
use Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeReflectionDefinitionBuilder;
|
||||
use Dedoc\Scramble\Infer\Reflector\ClassReflector;
|
||||
use Dedoc\Scramble\Infer\Scope\GlobalScope;
|
||||
use Dedoc\Scramble\Infer\Scope\Index;
|
||||
use Dedoc\Scramble\Infer\Scope\NodeTypesResolver;
|
||||
use Dedoc\Scramble\Infer\Scope\Scope;
|
||||
use Dedoc\Scramble\Infer\Scope\ScopeContext;
|
||||
use Dedoc\Scramble\Infer\Services\FileNameResolver;
|
||||
use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\IndexBuilders\IndexBuilder;
|
||||
use Dedoc\Scramble\Support\PhpDoc;
|
||||
use Dedoc\Scramble\Support\Type\FunctionType;
|
||||
use Dedoc\Scramble\Support\Type\Generic;
|
||||
use Dedoc\Scramble\Support\Type\ObjectType;
|
||||
use Dedoc\Scramble\Support\Type\TemplateType;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\TypeWalker;
|
||||
use Dedoc\Scramble\Support\Type\UnknownType;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpParser\ErrorHandler\Throwing;
|
||||
use PhpParser\NameContext;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
|
||||
|
||||
class ClassDefinition implements ClassDefinitionContract
|
||||
{
|
||||
private bool $propagatesTemplates = false;
|
||||
|
||||
/** @var array<string, true> */
|
||||
protected array $loadedMethods = [];
|
||||
|
||||
/** @var Collection<string, Collection<string, Type>> */
|
||||
private Collection $classContexts;
|
||||
|
||||
private Index $index;
|
||||
|
||||
public function __construct(
|
||||
// FQ name
|
||||
public string $name,
|
||||
/** @var TemplateType[] $templateTypes */
|
||||
public array $templateTypes = [],
|
||||
/** @var array<string, ClassPropertyDefinition> $properties */
|
||||
public array $properties = [],
|
||||
/** @var array<string, FunctionLikeDefinition> $methods */
|
||||
public array $methods = [],
|
||||
public ?string $parentFqn = null,
|
||||
) {}
|
||||
|
||||
public function propagatesTemplates(?bool $propagatesTemplates = null): bool
|
||||
{
|
||||
if ($propagatesTemplates === null) {
|
||||
return $this->propagatesTemplates;
|
||||
}
|
||||
|
||||
return $this->propagatesTemplates = $propagatesTemplates;
|
||||
}
|
||||
|
||||
public function isInstanceOf(string $className)
|
||||
{
|
||||
return is_a($this->name, $className, true);
|
||||
}
|
||||
|
||||
public function isChildOf(string $className)
|
||||
{
|
||||
return $this->isInstanceOf($className) && $this->name !== $className;
|
||||
}
|
||||
|
||||
public function hasMethodDefinition(string $name): bool
|
||||
{
|
||||
return $this->lazilyLoadMethodDefinition($name) || $this->findReflectionMethod($name);
|
||||
}
|
||||
|
||||
public function getMethodDefinitionWithoutAnalysis(string $name): ?FunctionLikeDefinition
|
||||
{
|
||||
return $this->lazilyLoadMethodDefinition($name);
|
||||
}
|
||||
|
||||
protected function lazilyLoadMethodDefinition(string $name): ?FunctionLikeDefinition
|
||||
{
|
||||
if (array_key_exists($name, $this->loadedMethods)) {
|
||||
return $this->methods[$name] ?? null;
|
||||
}
|
||||
|
||||
$this->loadedMethods[$name] = true;
|
||||
|
||||
/** @var \ReflectionMethod|null $reflectionMethod */
|
||||
$reflectionMethod = rescue(
|
||||
fn () => (new \ReflectionClass($this->name))->getMethod($name), // @phpstan-ignore argument.type
|
||||
report: false,
|
||||
);
|
||||
|
||||
if (! $reflectionMethod) {
|
||||
return $this->methods[$name] ?? null;
|
||||
}
|
||||
|
||||
if ($this->isMethodDefinedInNonAstAnalyzableTrait($name)) {
|
||||
return $this->methods[$name] ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
$reflectionMethod->class === $this->name
|
||||
|| Scramble::infer()->config->shouldAnalyzeAst($reflectionMethod->class)
|
||||
) {
|
||||
return $this->methods[$reflectionMethod->name] = new FunctionLikeDefinition(
|
||||
new FunctionType(
|
||||
$reflectionMethod->name,
|
||||
arguments: [],
|
||||
returnType: new UnknownType,
|
||||
),
|
||||
definingClassName: $reflectionMethod->class,
|
||||
isStatic: $reflectionMethod->isStatic(),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->methods[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getMethodDefiningClassName(string $name, Index $index)
|
||||
{
|
||||
$lastLookedUpClassName = $this->name;
|
||||
while ($lastLookedUpClassDefinition = $index->getClass($lastLookedUpClassName)) {
|
||||
if ($methodDefinition = $lastLookedUpClassDefinition->getMethodDefinitionWithoutAnalysis($name)) {
|
||||
return $methodDefinition->definingClassName;
|
||||
}
|
||||
|
||||
if ($lastLookedUpClassDefinition->parentFqn) {
|
||||
$lastLookedUpClassName = $lastLookedUpClassDefinition->parentFqn;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $lastLookedUpClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IndexBuilder<array<string, mixed>>[] $indexBuilders
|
||||
*/
|
||||
public function getMethodDefinition(string $name, Scope $scope = new GlobalScope, array $indexBuilders = [], bool $withSideEffects = false): ?FunctionLikeDefinition
|
||||
{
|
||||
if (! $methodDefinition = $this->lazilyLoadMethodDefinition($name)) {
|
||||
return $this->getFunctionLikeDefinitionBuiltFromReflection($name);
|
||||
}
|
||||
|
||||
return $this->getFunctionLikeDefinitionBuiltFromAst($methodDefinition, $name, $scope, $indexBuilders, $withSideEffects);
|
||||
}
|
||||
|
||||
protected function isMethodDefinedInNonAstAnalyzableTrait(string $name): bool
|
||||
{
|
||||
if (! $reflectionMethod = $this->findReflectionMethod($name)) {
|
||||
return false;
|
||||
}
|
||||
/** @var \ReflectionClass<object>|null $classReflection */
|
||||
$classReflection = rescue(fn () => new \ReflectionClass($this->name), report: false); // @phpstan-ignore argument.type
|
||||
|
||||
if (! $classReflection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isDeclaredIn($reflectionMethod, $classReflection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (class_uses_recursive($classReflection->name) as $traitName) {
|
||||
$traitReflection = rescue(
|
||||
fn () => new \ReflectionClass($traitName), // @phpstan-ignore argument.type
|
||||
report: false,
|
||||
);
|
||||
|
||||
if (! $traitReflection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $this->isDeclaredIn($reflectionMethod, $traitReflection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! Scramble::infer()->config->shouldAnalyzeAst($traitName);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionClass<object> $class
|
||||
*/
|
||||
private function isDeclaredIn(\ReflectionMethod $reflectionMethod, \ReflectionClass $class): bool
|
||||
{
|
||||
$reflectionMethodFileName = $reflectionMethod->getFileName();
|
||||
$reflectionMethodStartLine = $reflectionMethod->getStartLine();
|
||||
|
||||
$classFileName = $class->getFileName();
|
||||
$classStartLine = $class->getStartLine();
|
||||
$classEndLine = $class->getEndLine();
|
||||
|
||||
if (! $reflectionMethodFileName || ! $classFileName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($reflectionMethodFileName !== $classFileName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $reflectionMethodStartLine > $classStartLine && $reflectionMethodStartLine < $classEndLine;
|
||||
}
|
||||
|
||||
private function findReflectionMethod(string $name): ?\ReflectionMethod
|
||||
{
|
||||
/** @var \ReflectionClass<object>|null $classReflection */
|
||||
$classReflection = rescue(fn () => new \ReflectionClass($this->name), report: false); // @phpstan-ignore argument.type
|
||||
/** @var \ReflectionMethod|null $methodReflection */
|
||||
$methodReflection = rescue(fn () => $classReflection?->getMethod($name), report: false);
|
||||
|
||||
// The case when method is defined in the class or its parents.
|
||||
if ($methodReflection && $this->isDeclaredIn($methodReflection, $methodReflection->getDeclaringClass())) {
|
||||
return $methodReflection;
|
||||
}
|
||||
|
||||
foreach ($this->getClassContexts()->keys() as $class) {
|
||||
/** @var \ReflectionClass<object>|null $classReflection */
|
||||
$classReflection = rescue(fn () => new \ReflectionClass($class), report: false); // @phpstan-ignore argument.type
|
||||
/** @var \ReflectionMethod|null $traitMethodReflection */
|
||||
$traitMethodReflection = rescue(fn () => $classReflection?->getMethod($name), report: false);
|
||||
|
||||
if ($traitMethodReflection) {
|
||||
return $traitMethodReflection;
|
||||
}
|
||||
}
|
||||
|
||||
return $methodReflection;
|
||||
}
|
||||
|
||||
protected function getFunctionLikeDefinitionBuiltFromReflection(string $name): ?FunctionLikeDefinition
|
||||
{
|
||||
if (array_key_exists($name, $this->methods)) {
|
||||
return $this->methods[$name];
|
||||
}
|
||||
|
||||
if (! $methodReflection = $this->findReflectionMethod($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$definition = (new FunctionLikeReflectionDefinitionBuilder(
|
||||
$name,
|
||||
$methodReflection,
|
||||
collect($this->templateTypes)->keyBy->name
|
||||
->merge($this->getMethodContextTemplates($methodReflection)),
|
||||
))->build();
|
||||
|
||||
return $this->methods[$name] = $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IndexBuilder<array<string, mixed>>[] $indexBuilders
|
||||
*/
|
||||
protected function getFunctionLikeDefinitionBuiltFromAst(
|
||||
FunctionLikeDefinition $methodDefinition,
|
||||
string $name,
|
||||
Scope $scope = new GlobalScope,
|
||||
array $indexBuilders = [],
|
||||
bool $withSideEffects = false,
|
||||
): ?FunctionLikeDefinition {
|
||||
if (! $methodDefinition->isFullyAnalyzed()) {
|
||||
$this->methods[$name] = (new MethodAnalyzer(
|
||||
$scope->index,
|
||||
$this,
|
||||
))->analyze($methodDefinition, $indexBuilders, $withSideEffects);
|
||||
}
|
||||
|
||||
if (! $this->methods[$name]) { // @phpstan-ignore booleanNot.alwaysFalse
|
||||
return $this->methods[$name];
|
||||
}
|
||||
|
||||
if (! $this->methods[$name]->referencesResolved) {
|
||||
$methodScope = new Scope(
|
||||
$scope->index,
|
||||
new NodeTypesResolver,
|
||||
new ScopeContext($this, $methodDefinition),
|
||||
new FileNameResolver(
|
||||
class_exists($this->name)
|
||||
? ClassReflector::make($this->name)->getNameContext()
|
||||
: tap(new NameContext(new Throwing), fn (NameContext $nc) => $nc->startNamespace()),
|
||||
),
|
||||
);
|
||||
|
||||
FunctionLikeAstDefinitionBuilder::resolveFunctionParameterDefaults($methodScope, $this->methods[$name]);
|
||||
|
||||
FunctionLikeAstDefinitionBuilder::resolveFunctionReturnReferences($methodScope, $this->methods[$name]);
|
||||
|
||||
FunctionLikeAstDefinitionBuilder::resolveFunctionExceptions($methodScope, $this->methods[$name]);
|
||||
|
||||
$this->methods[$name]->referencesResolved = true;
|
||||
}
|
||||
|
||||
return $this->methods[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<string, Type>
|
||||
*/
|
||||
private function getMethodContextTemplates(\ReflectionMethod $methodReflection): Collection
|
||||
{
|
||||
$classContexts = $this->getClassContexts();
|
||||
|
||||
return $classContexts->get($methodReflection->class, collect());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $ignoreClasses
|
||||
* @return Collection<string, Collection<string, Type>>
|
||||
*/
|
||||
public function getClassContexts(array $ignoreClasses = []): Collection
|
||||
{
|
||||
if (isset($this->classContexts)) {
|
||||
return $this->classContexts;
|
||||
}
|
||||
|
||||
if (in_array($this->name, $ignoreClasses, true)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$reflector = ClassReflector::make($this->name);
|
||||
|
||||
$docComment = rescue(fn () => $reflector->getReflection()->getDocComment(), report: false) ?: '';
|
||||
|
||||
$classSource = $docComment."\n".rescue($reflector->getSource(...), '', report: false);
|
||||
|
||||
/** @var Collection<int, string> $tagsDoc */
|
||||
$tagsDoc = Str::matchAll('/@(?:use|extends|mixin)\s+[^\r\n*]+/', $classSource);
|
||||
$contextPhpDoc = $tagsDoc->map(fn ($s) => " * $s")->prepend('/**')->push('*/')->join("\n");
|
||||
|
||||
$nameContext = rescue($reflector->getNameContext(...), report: false);
|
||||
|
||||
$phpDoc = PhpDoc::parse(
|
||||
$contextPhpDoc,
|
||||
$nameContext ? new FileNameResolver($nameContext) : null,
|
||||
);
|
||||
|
||||
/** @var (ExtendsTagValueNode|UsesTagValueNode|MixinTagValueNode)[] $tags */
|
||||
$tags = [
|
||||
...array_values($phpDoc->getExtendsTagValues()),
|
||||
...array_values($phpDoc->getUsesTagValues()),
|
||||
...array_values($phpDoc->getMixinTagValues()),
|
||||
];
|
||||
|
||||
$classTemplatesByName = collect($this->templateTypes)->keyBy->name;
|
||||
|
||||
$types = collect($tags)
|
||||
->map(fn ($tag) => PhpDocTypeHelper::toType($tag->type))
|
||||
->filter(fn ($type) => $type instanceof ObjectType);
|
||||
|
||||
if ($this->parentFqn && ! $types->firstWhere('name', $this->parentFqn)) {
|
||||
$types->push(new ObjectType($this->parentFqn));
|
||||
}
|
||||
|
||||
$classContexts = collect();
|
||||
|
||||
foreach ($types as $type) {
|
||||
$classContext = collect();
|
||||
$type = ! $type instanceof Generic ? new Generic($type->name) : $type;
|
||||
|
||||
if ($definition = $this->getIndex()->getClass($type->name)) {
|
||||
foreach ($definition->templateTypes as $i => $templateType) {
|
||||
$concreteType = (new TypeWalker)->map(
|
||||
$type->templateTypes[$i]
|
||||
?? $this->templateTypes[$i]
|
||||
?? $templateType->default
|
||||
?? new UnknownType,
|
||||
fn ($t) => $t instanceof ObjectType ? $classTemplatesByName->get($t->name, $t) : $t,
|
||||
);
|
||||
|
||||
$classContext->offsetSet($templateType->name, $concreteType);
|
||||
}
|
||||
}
|
||||
|
||||
$classContexts->offsetSet($type->name, $classContext);
|
||||
}
|
||||
|
||||
foreach ($classContexts as $class => $localContext) {
|
||||
if (in_array($class, $ignoreClasses, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $classDef = $this->getIndex()->getClass($class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$classContext = $classDef->getClassContexts([...$ignoreClasses, $this->name]);
|
||||
|
||||
$localizedClassContext = $classContext->map->map(function ($type) use ($localContext) {
|
||||
return (new TypeWalker)->map(
|
||||
$type,
|
||||
fn ($t) => $type instanceof TemplateType
|
||||
? $localContext->get($type->name, new UnknownType)
|
||||
: $type,
|
||||
);
|
||||
});
|
||||
|
||||
$classContexts = $localizedClassContext->merge($classContexts);
|
||||
}
|
||||
|
||||
// $this->dumpContext($this, $classContexts);
|
||||
|
||||
return $this->classContexts = $classContexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<string, Collection<string, Type>> $classContexts
|
||||
*/
|
||||
private function dumpContext(ClassDefinition $def, Collection $classContexts): void // @phpstan-ignore method.unused
|
||||
{
|
||||
$className = $def->name.($def->templateTypes ? '<'.implode(',', array_map($this->dumpTemplateName(...), $def->templateTypes)).'>' : '');
|
||||
|
||||
$contextNames = $classContexts->map(function ($contexts, $name) {
|
||||
$templates = $contexts->map(fn ($t) => $t instanceof TemplateType ? $this->dumpTemplateName($t) : $t->toString());
|
||||
|
||||
return $name.($templates->count() ? '<'.$templates->join(',').'>' : '');
|
||||
})->values()->toArray();
|
||||
|
||||
dump([
|
||||
$className => $contextNames,
|
||||
]);
|
||||
}
|
||||
|
||||
private function dumpTemplateName(TemplateType $tt): string
|
||||
{
|
||||
return $tt->name.'#'.spl_object_id($tt);
|
||||
}
|
||||
|
||||
public function setIndex(Index $index): void
|
||||
{
|
||||
$this->index = $index;
|
||||
}
|
||||
|
||||
protected function getIndex(): Index
|
||||
{
|
||||
return isset($this->index) ? $this->index : app(Index::class);
|
||||
}
|
||||
|
||||
public function getPropertyDefinition($name)
|
||||
{
|
||||
return $this->properties[$name] ?? null;
|
||||
}
|
||||
|
||||
public function hasPropertyDefinition(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->properties);
|
||||
}
|
||||
|
||||
public function getMethodCallType(string $name)
|
||||
{
|
||||
return $this->getMethodDefinition($name)?->getReturnType()
|
||||
?: new UnknownType("Cannot get type of calling method [$name] on object [$this->name]");
|
||||
}
|
||||
|
||||
public function getMethod(string $name): ?FunctionLikeDefinition
|
||||
{
|
||||
return $this->getMethodDefinition($name);
|
||||
}
|
||||
|
||||
public function getData(): ClassDefinition
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
|
||||
|
||||
class ClassPropertyDefinition
|
||||
{
|
||||
private ?PhpDocNode $docNode = null;
|
||||
|
||||
public function __construct(
|
||||
public Type $type,
|
||||
public ?Type $defaultType = null,
|
||||
public readonly bool $isStatic = false,
|
||||
public readonly PropertyVisibility $visibility = PropertyVisibility::Public,
|
||||
/** @var AttributeDefinition[] */
|
||||
public readonly array $attributes = [],
|
||||
private readonly ?PendingDocComment $pendingDocComment = null,
|
||||
) {}
|
||||
|
||||
public function getDocNode(): ?PhpDocNode
|
||||
{
|
||||
if (! $this->pendingDocComment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->docNode ??= $this->pendingDocComment->resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AttributeDefinition[]
|
||||
*/
|
||||
public function getAttributes(?string $name = null, int $flags = 0): array
|
||||
{
|
||||
if ($name === null) {
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
return array_values(array_filter(
|
||||
$this->attributes,
|
||||
function (AttributeDefinition $attribute) use ($name, $flags) {
|
||||
if ($flags & AttributeDefinition::IS_INSTANCEOF) {
|
||||
return is_a($attribute->name, $name, true);
|
||||
}
|
||||
|
||||
return $attribute->name === $name;
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use Dedoc\Scramble\Infer\Flow\Nodes;
|
||||
use Dedoc\Scramble\Infer\FlowBuilder;
|
||||
use Dedoc\Scramble\Infer\Scope\Scope;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\UnknownType;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use PhpParser\NodeTraverser;
|
||||
|
||||
class FunctionLikeAstDefinition extends FunctionLikeDefinition
|
||||
{
|
||||
private ?FunctionLikeDefinition $declarationDefinition = null;
|
||||
|
||||
private ?Scope $scope = null;
|
||||
|
||||
private ?FunctionLike $astNode = null;
|
||||
|
||||
private ?Nodes $flowContainer = null;
|
||||
|
||||
public function setDeclarationDefinition(?FunctionLikeDefinition $declarationDefinition): self
|
||||
{
|
||||
$this->declarationDefinition = $declarationDefinition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeclarationDefinition(): ?FunctionLikeDefinition
|
||||
{
|
||||
return $this->declarationDefinition;
|
||||
}
|
||||
|
||||
public function setAstNode(FunctionLike $astNode): self
|
||||
{
|
||||
$this->astNode = $astNode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAstNode(): FunctionLike
|
||||
{
|
||||
if (! $this->astNode) {
|
||||
/**
|
||||
* @see \Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeAstDefinitionBuilder
|
||||
*/
|
||||
throw new \LogicException('AST node must be set before accessing it on FunctionLikeAstDefinition');
|
||||
}
|
||||
|
||||
return $this->astNode;
|
||||
}
|
||||
|
||||
public function setScope(Scope $scope): self
|
||||
{
|
||||
$this->scope = $scope;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getScope(): Scope
|
||||
{
|
||||
if (! $this->scope) {
|
||||
/**
|
||||
* @see \Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeAstDefinitionBuilder
|
||||
*/
|
||||
throw new \LogicException('Scope must be set before accessing it on FunctionLikeAstDefinition');
|
||||
}
|
||||
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function getFlowContainer(): Nodes
|
||||
{
|
||||
if ($this->flowContainer) {
|
||||
return $this->flowContainer;
|
||||
}
|
||||
|
||||
$traverser = new NodeTraverser;
|
||||
|
||||
$traverser->addVisitor($flowBuilder = new FlowBuilder(
|
||||
$this->type->arguments, // parameters
|
||||
$this->getScope(),
|
||||
));
|
||||
|
||||
$traverser->traverse([$this->getAstNode()]);
|
||||
|
||||
return $this->flowContainer = $flowBuilder->flowNodes;
|
||||
}
|
||||
|
||||
public function getInferredReturnType(): Type
|
||||
{
|
||||
return $this->type->getReturnType();
|
||||
}
|
||||
|
||||
public function getReturnType(): Type
|
||||
{
|
||||
$inferredReturnType = $this->type->getReturnType();
|
||||
|
||||
if ($inferredReturnType->getAttribute('fromScrambleReturn') === true) {
|
||||
return $inferredReturnType;
|
||||
}
|
||||
|
||||
if (! $returnDeclarationType = $this->getDeclarationDefinition()?->getReturnType()) {
|
||||
return $inferredReturnType;
|
||||
}
|
||||
|
||||
return $this->prefersInferredReturnType($returnDeclarationType, $inferredReturnType)
|
||||
? $inferredReturnType
|
||||
: $returnDeclarationType;
|
||||
}
|
||||
|
||||
private function prefersInferredReturnType(?Type $declarationType, Type $inferredType): bool
|
||||
{
|
||||
if (! $declarationType || $declarationType instanceof UnknownType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($declarationType->accepts($inferredType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($inferredType->acceptedBy($declarationType)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use Dedoc\Scramble\Infer\DefinitionBuilders\SelfOutTypeBuilder;
|
||||
use Dedoc\Scramble\Support\Type\FunctionType;
|
||||
use Dedoc\Scramble\Support\Type\Generic;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
|
||||
class FunctionLikeDefinition
|
||||
{
|
||||
public bool $isFullyAnalyzed = false;
|
||||
|
||||
public bool $referencesResolved = false;
|
||||
|
||||
public ?Generic $selfOutType;
|
||||
|
||||
/**
|
||||
* @param array<string, Type> $argumentsDefaults A map where the key is arg name and value is a default type.
|
||||
*/
|
||||
public function __construct(
|
||||
public FunctionType $type,
|
||||
public array $argumentsDefaults = [],
|
||||
public ?string $definingClassName = null,
|
||||
public bool $isStatic = false,
|
||||
public ?SelfOutTypeBuilder $selfOutTypeBuilder = null,
|
||||
) {}
|
||||
|
||||
public function isFullyAnalyzed(): bool
|
||||
{
|
||||
return $this->isFullyAnalyzed;
|
||||
}
|
||||
|
||||
public function addArgumentDefault(string $paramName, Type $type): self
|
||||
{
|
||||
$this->argumentsDefaults[$paramName] = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSelfOutType(): ?Generic
|
||||
{
|
||||
return $this->selfOutType ??= $this->selfOutTypeBuilder?->build();
|
||||
}
|
||||
|
||||
public function getReturnType(): Type
|
||||
{
|
||||
return $this->type->getReturnType();
|
||||
}
|
||||
|
||||
/**
|
||||
* When analyzing parent classes, function like definitions are "copied" from parent class. When function
|
||||
* like is sourced from AST, we don't want to make a deep clone to save some memory (is the difference really makes sense?)
|
||||
* as the definition will be re-build to the specifics of a given class. However, other types of fn definitions
|
||||
* may override this method and do a deeper cloning (or cloning at all!).
|
||||
*/
|
||||
public function copyFromParent(): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use Dedoc\Scramble\Infer\Contracts\ClassDefinition as ClassDefinitionContract;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition as ClassDefinitionData;
|
||||
use Dedoc\Scramble\Infer\DefinitionBuilders\FunctionLikeReflectionDefinitionBuilder;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
class LazyShallowClassDefinition implements ClassDefinitionContract
|
||||
{
|
||||
public function __construct(
|
||||
public ClassDefinitionContract $definition,
|
||||
) {}
|
||||
|
||||
public function getMethod(string $name): ?FunctionLikeDefinition
|
||||
{
|
||||
$data = $this->getData();
|
||||
|
||||
if (isset($data->methods[$name])) {
|
||||
return $data->methods[$name];
|
||||
}
|
||||
|
||||
try {
|
||||
$reflection = (new ReflectionClass($data->name))->getMethod($name);
|
||||
} catch (ReflectionException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data->methods[$name] = (new FunctionLikeReflectionDefinitionBuilder($name, $reflection))->build();
|
||||
}
|
||||
|
||||
public function getData(): ClassDefinitionData
|
||||
{
|
||||
return $this->definition->getData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use Dedoc\Scramble\Infer\Services\FileNameResolver;
|
||||
use Dedoc\Scramble\Support\PhpDoc;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
|
||||
|
||||
class PendingDocComment
|
||||
{
|
||||
public function __construct(
|
||||
public string $docComment,
|
||||
public ?string $fileName = null,
|
||||
public ?string $declaringClass = null,
|
||||
) {}
|
||||
|
||||
public function resolve(): PhpDocNode
|
||||
{
|
||||
$fileName = $this->resolveFileName();
|
||||
|
||||
return PhpDoc::parse(
|
||||
$this->docComment,
|
||||
$fileName ? FileNameResolver::createForFile($fileName) : null,
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveFileName(): ?string
|
||||
{
|
||||
if ($this->fileName) {
|
||||
return $this->fileName;
|
||||
}
|
||||
|
||||
if ($this->declaringClass) {
|
||||
return $this->getDeclaringClassFile($this->declaringClass);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getDeclaringClassFile(string $declaringClass): ?string
|
||||
{
|
||||
try {
|
||||
$reflectionClass = new \ReflectionClass($declaringClass);
|
||||
|
||||
return $reflectionClass->getFileName() ?: null;
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use ReflectionProperty;
|
||||
|
||||
enum PropertyVisibility
|
||||
{
|
||||
case Private;
|
||||
case Protected;
|
||||
case Public;
|
||||
|
||||
public static function fromReflectionProperty(ReflectionProperty $property): self
|
||||
{
|
||||
return match (true) {
|
||||
$property->isPrivate() => self::Private,
|
||||
$property->isProtected() => self::Protected,
|
||||
default => self::Public,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\Definition;
|
||||
|
||||
use Dedoc\Scramble\Infer\Scope\GlobalScope;
|
||||
use Dedoc\Scramble\Infer\Scope\Scope;
|
||||
|
||||
class ShallowClassDefinition extends ClassDefinition
|
||||
{
|
||||
protected function lazilyLoadMethodDefinition(string $name): ?FunctionLikeDefinition
|
||||
{
|
||||
return $this->methods[$name] ?? null;
|
||||
}
|
||||
|
||||
public function getMethodDefinition(string $name, Scope $scope = new GlobalScope, array $indexBuilders = [], bool $withSideEffects = false): ?FunctionLikeDefinition
|
||||
{
|
||||
if ($this->isMethodDefinedInNonAstAnalyzableTrait($name)) {
|
||||
return $this->getFunctionLikeDefinitionBuiltFromReflection($name);
|
||||
}
|
||||
|
||||
return $this->getMethodDefinitionWithoutAnalysis($name) ?: $this->getFunctionLikeDefinitionBuiltFromReflection($name);
|
||||
}
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\DefinitionBuilders;
|
||||
|
||||
use Dedoc\Scramble\Infer\Context;
|
||||
use Dedoc\Scramble\Infer\Contracts\FunctionLikeDefinitionBuilder;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeAstDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
use Dedoc\Scramble\Infer\Extensions\Event\MethodCallEvent;
|
||||
use Dedoc\Scramble\Infer\Extensions\Event\SideEffectCallEvent;
|
||||
use Dedoc\Scramble\Infer\Extensions\ExtensionsBroker;
|
||||
use Dedoc\Scramble\Infer\Handler\IndexBuildingHandler;
|
||||
use Dedoc\Scramble\Infer\Scope\Index;
|
||||
use Dedoc\Scramble\Infer\Scope\LazyShallowReflectionIndex;
|
||||
use Dedoc\Scramble\Infer\Scope\NodeTypesResolver;
|
||||
use Dedoc\Scramble\Infer\Scope\Scope;
|
||||
use Dedoc\Scramble\Infer\Scope\ScopeContext;
|
||||
use Dedoc\Scramble\Infer\Services\FileNameResolver;
|
||||
use Dedoc\Scramble\Infer\Services\ReferenceTypeResolver;
|
||||
use Dedoc\Scramble\Infer\Services\ShallowTypeResolver;
|
||||
use Dedoc\Scramble\Infer\TypeInferer;
|
||||
use Dedoc\Scramble\Infer\UnresolvableArgumentTypeBag;
|
||||
use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
|
||||
use Dedoc\Scramble\Support\IndexBuilders\IndexBuilder;
|
||||
use Dedoc\Scramble\Support\Type\ObjectType;
|
||||
use Dedoc\Scramble\Support\Type\TemplateType;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\TypeWalker;
|
||||
use Illuminate\Support\Arr;
|
||||
use LogicException;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\New_;
|
||||
use PhpParser\Node\Expr\NullsafeMethodCall;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use PhpParser\Node\Identifier;
|
||||
use PhpParser\Node\Name;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
|
||||
use ReflectionClass;
|
||||
use ReflectionFunction;
|
||||
use ReflectionMethod;
|
||||
|
||||
class FunctionLikeAstDefinitionBuilder implements FunctionLikeDefinitionBuilder
|
||||
{
|
||||
private LazyShallowReflectionIndex $shallowIndex;
|
||||
|
||||
/**
|
||||
* @param IndexBuilder<array<string, mixed>>[] $indexBuilders
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public FunctionLike $functionLike,
|
||||
public Index $index,
|
||||
public FileNameResolver $fileNameResolver,
|
||||
public ?ClassDefinition $classDefinition = null,
|
||||
public array $indexBuilders = [],
|
||||
public bool $withSideEffects = false,
|
||||
) {
|
||||
$this->shallowIndex = app(LazyShallowReflectionIndex::class);
|
||||
}
|
||||
|
||||
public function build(): FunctionLikeAstDefinition
|
||||
{
|
||||
$inferrer = $this->traverseAstNode($this->functionLike);
|
||||
|
||||
$scope = $inferrer->getFunctionLikeScope($this->functionLike);
|
||||
|
||||
if (! $scope) {
|
||||
throw new LogicException('Scope must be available when building FunctionLikeAstDefinition');
|
||||
}
|
||||
|
||||
$definition = $scope->context->functionDefinition;
|
||||
|
||||
if (! $definition instanceof FunctionLikeAstDefinition) {
|
||||
throw new LogicException('Definition must be an instance of FunctionLikeAstDefinition');
|
||||
}
|
||||
|
||||
$definition
|
||||
->setAstNode($this->functionLike)
|
||||
->setScope($scope);
|
||||
|
||||
if ($this->functionLike instanceof ClassMethod) {
|
||||
$definition->selfOutTypeBuilder = new SelfOutTypeBuilder($scope, $this->functionLike);
|
||||
}
|
||||
|
||||
$definition->setDeclarationDefinition($this->buildDeclarationDefinition());
|
||||
|
||||
$this->overrideInferredReturnTypeWithManualAnnotation($definition);
|
||||
|
||||
if ($this->withSideEffects) {
|
||||
$this->analyzeSideEffects($definition, $inferrer);
|
||||
}
|
||||
|
||||
$definition->isFullyAnalyzed = true;
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
private function traverseAstNode(Node $node): TypeInferer
|
||||
{
|
||||
$traverser = new NodeTraverser;
|
||||
|
||||
$traverser->addVisitor($inferrer = new TypeInferer(
|
||||
$this->index,
|
||||
$this->fileNameResolver,
|
||||
new Scope($this->index, new NodeTypesResolver, new ScopeContext($this->classDefinition), $this->fileNameResolver),
|
||||
Context::getInstance()->extensionsBroker->extensions,
|
||||
[new IndexBuildingHandler($this->indexBuilders)],
|
||||
));
|
||||
|
||||
$traverser->traverse(Arr::wrap($node));
|
||||
|
||||
return $inferrer;
|
||||
}
|
||||
|
||||
private function analyzeSideEffects(FunctionLikeDefinition $methodDefinition, TypeInferer $inferrer): void
|
||||
{
|
||||
$fnScope = $inferrer->getFunctionLikeScope($this->functionLike);
|
||||
|
||||
if (! $fnScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($fnScope->getMethodCalls() as $methodCall) {
|
||||
match (true) {
|
||||
$methodCall instanceof MethodCall || $methodCall instanceof NullsafeMethodCall => $this->analyzeMethodCall($methodDefinition, $fnScope, $methodCall),
|
||||
$methodCall instanceof StaticCall => $this->analyzeStaticMethodCall($methodDefinition, $fnScope, $methodCall),
|
||||
$methodCall instanceof FuncCall => $this->analyzeFuncCall($methodDefinition, $fnScope, $methodCall),
|
||||
$methodCall instanceof New_ => null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private function analyzeMethodCall(FunctionLikeDefinition $methodDefinition, Scope $fnScope, MethodCall|NullsafeMethodCall $methodCall): void
|
||||
{
|
||||
// 1. ensure method call should be handled
|
||||
/*
|
||||
* Only explicit method calls are supported. So the following is supported:
|
||||
* $this->foo()
|
||||
* But when the expression is in place, we skip analysis:
|
||||
* $this->{$var}()
|
||||
*/
|
||||
$this->applyExceptionsFromMethodCall($methodDefinition, $fnScope, $methodCall);
|
||||
|
||||
if (! $methodCall->name instanceof Identifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. get called method definition and if not yet analyzed, analyze shallowly (PHPDoc, type hints)
|
||||
|
||||
// get shallow method definition (get shallow callee type, get the shallow definition)
|
||||
$calleeType = (new ShallowTypeResolver($this->shallowIndex))->resolve($fnScope, $fnScope->getType($methodCall->var));
|
||||
if ($calleeType instanceof TemplateType && $calleeType->is) {
|
||||
$calleeType = $calleeType->is;
|
||||
}
|
||||
if (! $calleeType instanceof ObjectType) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $this->shallowIndex->getClass($calleeType->name);
|
||||
if (! $definition) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shallowMethodDefinition = $definition->getMethod($methodCall->name->name);
|
||||
if (! $shallowMethodDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applySideEffectsFromCall(new SideEffectCallEvent(
|
||||
definition: $methodDefinition,
|
||||
calledDefinition: $shallowMethodDefinition,
|
||||
node: $methodCall,
|
||||
scope: $fnScope,
|
||||
arguments: new UnresolvableArgumentTypeBag($fnScope->getArgsTypes($methodCall->args)),
|
||||
));
|
||||
}
|
||||
|
||||
private function applyExceptionsFromMethodCall(FunctionLikeDefinition $methodDefinition, Scope $fnScope, MethodCall|NullsafeMethodCall $methodCall): void
|
||||
{
|
||||
if (! $methodCall->name instanceof Identifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
$calleeType = $fnScope->getType($methodCall->var);
|
||||
|
||||
if (! $calleeType instanceof ObjectType) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new MethodCallEvent(
|
||||
$calleeType,
|
||||
$methodCall->name->name,
|
||||
$fnScope,
|
||||
new UnresolvableArgumentTypeBag($fnScope->getArgsTypes($methodCall->args)),
|
||||
$calleeType->name
|
||||
);
|
||||
|
||||
$exceptions = app(ExtensionsBroker::class)->getMethodCallExceptions($event);
|
||||
|
||||
if (empty($exceptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$methodDefinition->type->exceptions = array_merge(
|
||||
$methodDefinition->type->exceptions,
|
||||
$exceptions,
|
||||
);
|
||||
}
|
||||
|
||||
private function analyzeStaticMethodCall(FunctionLikeDefinition $methodDefinition, Scope $fnScope, StaticCall $methodCall): void
|
||||
{
|
||||
if (! $methodCall->name instanceof Identifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $methodCall->class instanceof Name) {
|
||||
return;
|
||||
}
|
||||
|
||||
$class = ReferenceTypeResolver::resolveClassName($fnScope, $methodCall->class->name);
|
||||
if (! $class) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $this->shallowIndex->getClass($class);
|
||||
if (! $definition) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shallowMethodDefinition = $definition->getMethod($methodCall->name->name);
|
||||
if (! $shallowMethodDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applySideEffectsFromCall(new SideEffectCallEvent(
|
||||
definition: $methodDefinition,
|
||||
calledDefinition: $shallowMethodDefinition,
|
||||
node: $methodCall,
|
||||
scope: $fnScope,
|
||||
arguments: new UnresolvableArgumentTypeBag($fnScope->getArgsTypes($methodCall->args)),
|
||||
));
|
||||
}
|
||||
|
||||
private function analyzeFuncCall(FunctionLikeDefinition $methodDefinition, Scope $fnScope, FuncCall $call): void
|
||||
{
|
||||
$name = $call->name->getAttribute('namespacedName', $call->name);
|
||||
if (! $name instanceof Name) {
|
||||
return;
|
||||
}
|
||||
|
||||
$functionDefinition = $this->shallowIndex->getFunction($name);
|
||||
if (! $functionDefinition) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->applySideEffectsFromCall(new SideEffectCallEvent(
|
||||
definition: $methodDefinition,
|
||||
calledDefinition: $functionDefinition,
|
||||
node: $call,
|
||||
scope: $fnScope,
|
||||
arguments: new UnresolvableArgumentTypeBag($fnScope->getArgsTypes($call->args)),
|
||||
));
|
||||
}
|
||||
|
||||
private function applySideEffectsFromCall(SideEffectCallEvent $event): void
|
||||
{
|
||||
foreach ($event->calledDefinition->type->exceptions as $exception) {
|
||||
$event->definition->type->exceptions[] = $exception;
|
||||
}
|
||||
|
||||
Context::getInstance()->extensionsBroker->afterSideEffectCallAnalyzed($event);
|
||||
}
|
||||
|
||||
private function overrideInferredReturnTypeWithManualAnnotation(FunctionLikeDefinition $definition): void
|
||||
{
|
||||
if (! $type = $this->getExplicitScrambleReturnType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition->type->setReturnType($type);
|
||||
}
|
||||
|
||||
protected function buildDeclarationDefinition(): ?FunctionLikeDefinition
|
||||
{
|
||||
$definition = (new FunctionLikeDeclarationAstDefinitionBuilder(
|
||||
$this->functionLike,
|
||||
$this->classDefinition,
|
||||
))->build();
|
||||
|
||||
$phpDocNode = $this->functionLike->getAttribute('parsedPhpDoc');
|
||||
if (! $phpDocNode instanceof PhpDocNode) {
|
||||
return $definition;
|
||||
}
|
||||
|
||||
return (new FunctionLikeDeclarationPhpDocDefinitionBuilder(
|
||||
$definition,
|
||||
$phpDocNode,
|
||||
$this->classDefinition,
|
||||
))->build();
|
||||
}
|
||||
|
||||
protected function getReflection(): ReflectionFunction|ReflectionMethod|null
|
||||
{
|
||||
try {
|
||||
if ($this->classDefinition) {
|
||||
return (new ReflectionClass($this->classDefinition->name))->getMethod($this->name); // @phpstan-ignore argument.type
|
||||
}
|
||||
|
||||
return new ReflectionFunction($this->name);
|
||||
} catch (\ReflectionException) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getExplicitScrambleReturnType(): ?Type
|
||||
{
|
||||
$phpDoc = $this->functionLike->getAttribute('parsedPhpDoc');
|
||||
if (! $phpDoc instanceof PhpDocNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $scrambleReturn = Arr::first($phpDoc->getReturnTagValues('@scramble-return'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var ReturnTagValueNode $scrambleReturn */
|
||||
$type = PhpDocTypeHelper::toType($scrambleReturn->type);
|
||||
foreach (($this->classDefinition?->templateTypes ?: []) as $template) {
|
||||
$type = (new TypeWalker)->map($type, fn ($t) => $t instanceof ObjectType && $t->name === $template->name ? $template : $t);
|
||||
}
|
||||
|
||||
$type->setAttribute('fromScrambleReturn', true);
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
public static function resolveFunctionExceptions(Scope $scope, FunctionLikeDefinition $functionLikeDefinition): void
|
||||
{
|
||||
$functionType = $functionLikeDefinition->type;
|
||||
|
||||
foreach ($functionType->exceptions as $i => $exceptionType) {
|
||||
$exception = (new ReferenceTypeResolver($scope->index))->resolve($scope, $exceptionType);
|
||||
if (! $exception instanceof ObjectType) {
|
||||
continue;
|
||||
}
|
||||
$functionType->exceptions[$i] = $exception;
|
||||
}
|
||||
}
|
||||
|
||||
public static function resolveFunctionReturnReferences(Scope $scope, FunctionLikeDefinition $functionLikeDefinition): void
|
||||
{
|
||||
$functionType = $functionLikeDefinition->type;
|
||||
|
||||
$returnType = $functionType->getReturnType();
|
||||
$resolvedReference = ReferenceTypeResolver::getInstance()->resolve($scope, $returnType);
|
||||
$functionType->setReturnType($resolvedReference);
|
||||
}
|
||||
|
||||
public static function resolveFunctionParameterDefaults(Scope $scope, FunctionLikeDefinition $functionLikeDefinition): void
|
||||
{
|
||||
$referenceTypeResolver = ReferenceTypeResolver::getInstance();
|
||||
|
||||
foreach ($functionLikeDefinition->argumentsDefaults as $name => $argumentDefault) {
|
||||
$functionLikeDefinition->argumentsDefaults[$name] = $referenceTypeResolver->resolve($scope, $argumentDefault);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\DefinitionBuilders;
|
||||
|
||||
use Dedoc\Scramble\Infer\Contracts\FunctionLikeDefinitionBuilder;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
use Dedoc\Scramble\Infer\Scope\GlobalScope;
|
||||
use Dedoc\Scramble\Support\Type\FunctionType;
|
||||
use Dedoc\Scramble\Support\Type\MixedType;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\TypeHelper;
|
||||
use Dedoc\Scramble\Support\Type\UnknownType;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\FunctionLike;
|
||||
use PhpParser\Node\Stmt\ClassMethod;
|
||||
|
||||
class FunctionLikeDeclarationAstDefinitionBuilder implements FunctionLikeDefinitionBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private FunctionLike $node,
|
||||
private ?ClassDefinition $classDefinition = null,
|
||||
) {}
|
||||
|
||||
public function build(): FunctionLikeDefinition
|
||||
{
|
||||
return new FunctionLikeDefinition(
|
||||
$this->buildType(),
|
||||
argumentsDefaults: $this->getArgumentDefaults(),
|
||||
definingClassName: $this->classDefinition?->name,
|
||||
isStatic: $this->node instanceof Node\Stmt\ClassMethod ? $this->node->isStatic() : false,
|
||||
);
|
||||
}
|
||||
|
||||
private function buildType(): FunctionType
|
||||
{
|
||||
return new FunctionType(
|
||||
$this->getName(),
|
||||
arguments: $this->getArgumentTypes(),
|
||||
returnType: $this->getReturnType(),
|
||||
);
|
||||
}
|
||||
|
||||
private function getReturnType(): Type
|
||||
{
|
||||
if (! $returnType = $this->node->getReturnType()) {
|
||||
return new UnknownType;
|
||||
}
|
||||
|
||||
return TypeHelper::createTypeFromTypeNode($returnType);
|
||||
}
|
||||
|
||||
/** @return array<string, Type> */
|
||||
private function getArgumentTypes(): array
|
||||
{
|
||||
return collect($this->node->getParams())
|
||||
->mapWithKeys(function (Node\Param $param) {
|
||||
if (! $param->var instanceof Node\Expr\Variable || ! is_string($param->var->name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$type = $param->type
|
||||
? TypeHelper::createTypeFromTypeNode($param->type)
|
||||
: new MixedType;
|
||||
|
||||
return [$param->var->name => $type];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/** @return array<string, Type> */
|
||||
private function getArgumentDefaults(): array
|
||||
{
|
||||
return collect($this->node->getParams())
|
||||
->mapWithKeys(function (Node\Param $param) {
|
||||
if (! $param->default) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! $param->var instanceof Node\Expr\Variable || ! is_string($param->var->name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
$param->var->name => (new GlobalScope)->getType($param->default),
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function getName(): string
|
||||
{
|
||||
if ($this->node instanceof ClassMethod || $this->node instanceof Node\Stmt\Function_) {
|
||||
return $this->node->name->name;
|
||||
}
|
||||
|
||||
return '{anonymous}';
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\DefinitionBuilders;
|
||||
|
||||
use Dedoc\Scramble\Infer\Contracts\FunctionLikeDefinitionBuilder;
|
||||
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
|
||||
use Dedoc\Scramble\Support\Type\ObjectType;
|
||||
use Dedoc\Scramble\Support\Type\TemplateType;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\TypeWalker;
|
||||
use Illuminate\Support\Str;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
|
||||
|
||||
class FunctionLikeDeclarationPhpDocDefinitionBuilder implements FunctionLikeDefinitionBuilder
|
||||
{
|
||||
private PhpDocNode $phpDocNode;
|
||||
|
||||
public function __construct(
|
||||
private FunctionLikeDefinition $definition,
|
||||
?PhpDocNode $phpDocNode = null,
|
||||
private ?ClassDefinition $classDefinition = null,
|
||||
) {
|
||||
$this->phpDocNode = $phpDocNode ?: new PhpDocNode([]);
|
||||
}
|
||||
|
||||
public function build(): FunctionLikeDefinition
|
||||
{
|
||||
$definition = clone $this->definition;
|
||||
$definition->type = $definition->type->clone();
|
||||
|
||||
$phpDoc = $this->phpDocNode;
|
||||
|
||||
foreach ($phpDoc->getTemplateTagValues() as $templateTagValue) {
|
||||
$definition->type->templates[] = new TemplateType(
|
||||
name: $templateTagValue->name,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($phpDoc->getParamTagValues() as $paramTagValue) {
|
||||
$name = Str::replaceFirst('$ ', '', $paramTagValue->parameterName);
|
||||
if (! array_key_exists($name, $definition->type->arguments)) {
|
||||
continue;
|
||||
}
|
||||
$definition->type->arguments[$name] = $this->handleStatic(
|
||||
PhpDocTypeHelper::toType($paramTagValue->type),
|
||||
$definition->type->templates,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($phpDoc->getThrowsTagValues() as $throwsTagValue) {
|
||||
$definition->type->exceptions[] = $this->handleStatic(
|
||||
PhpDocTypeHelper::toType($throwsTagValue->type),
|
||||
$definition->type->templates,
|
||||
);
|
||||
}
|
||||
|
||||
if ($returnTagValues = array_values($phpDoc->getReturnTagValues())) {
|
||||
$definition->type->returnType = $this->handleStatic(
|
||||
PhpDocTypeHelper::toType($returnTagValues[0]->type),
|
||||
$definition->type->templates,
|
||||
);
|
||||
}
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TemplateType[] $functionTemplates
|
||||
*/
|
||||
private function handleStatic(Type $type, array $functionTemplates): Type
|
||||
{
|
||||
$classTemplates = collect($this->classDefinition?->templateTypes ?: [])->keyBy->name;
|
||||
$functionTemplatesByKeys = collect($functionTemplates)->keyBy->name;
|
||||
|
||||
return (new TypeWalker)->map($type, function (Type $t) use ($functionTemplatesByKeys, $classTemplates) {
|
||||
if (! $t instanceof ObjectType) {
|
||||
return $t;
|
||||
}
|
||||
|
||||
$t->name = ltrim($t->name, '\\');
|
||||
|
||||
if ($definedTemplate = $classTemplates->get($t->name)) {
|
||||
return $definedTemplate;
|
||||
}
|
||||
|
||||
if ($definedFnTemplate = $functionTemplatesByKeys->get($t->name)) {
|
||||
return $definedFnTemplate;
|
||||
}
|
||||
|
||||
return $t;
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+170
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Dedoc\Scramble\Infer\DefinitionBuilders;
|
||||
|
||||
use Dedoc\Scramble\Infer\Contracts\FunctionLikeDefinitionBuilder;
|
||||
use Dedoc\Scramble\Infer\Definition\FunctionLikeDefinition;
|
||||
use Dedoc\Scramble\Infer\Services\FileNameResolver;
|
||||
use Dedoc\Scramble\PhpDoc\PhpDocTypeHelper;
|
||||
use Dedoc\Scramble\Support\PhpDoc;
|
||||
use Dedoc\Scramble\Support\Type\FunctionType;
|
||||
use Dedoc\Scramble\Support\Type\Generic;
|
||||
use Dedoc\Scramble\Support\Type\MixedType;
|
||||
use Dedoc\Scramble\Support\Type\ObjectType;
|
||||
use Dedoc\Scramble\Support\Type\TemplateType;
|
||||
use Dedoc\Scramble\Support\Type\Type;
|
||||
use Dedoc\Scramble\Support\Type\TypeHelper;
|
||||
use Dedoc\Scramble\Support\Type\TypeWalker;
|
||||
use Dedoc\Scramble\Support\Type\UnknownType;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
|
||||
use ReflectionFunction;
|
||||
use ReflectionMethod;
|
||||
use ReflectionParameter;
|
||||
|
||||
class FunctionLikeReflectionDefinitionBuilder implements FunctionLikeDefinitionBuilder
|
||||
{
|
||||
private ReflectionFunction|ReflectionMethod $reflection;
|
||||
|
||||
/** @var Collection<string, covariant Type> */
|
||||
private Collection $classTemplates;
|
||||
|
||||
/**
|
||||
* @param Collection<string, covariant Type>|null $classTemplates
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
ReflectionFunction|ReflectionMethod|null $reflection = null,
|
||||
?Collection $classTemplates = null,
|
||||
) {
|
||||
$this->reflection = $reflection ?: new ReflectionFunction($this->name);
|
||||
$this->classTemplates = $classTemplates ?: collect();
|
||||
}
|
||||
|
||||
public function build(): FunctionLikeDefinition
|
||||
{
|
||||
$functionDefinition = $this->buildFunctionDefinitionFromTypeHints();
|
||||
|
||||
$this->applyPhpDoc(
|
||||
$functionDefinition,
|
||||
PhpDoc::parse($this->reflection->getDocComment() ?: '/** */', FileNameResolver::createForFile($this->reflection->getFileName())),
|
||||
);
|
||||
|
||||
$functionDefinition->isFullyAnalyzed = true;
|
||||
|
||||
return $functionDefinition;
|
||||
}
|
||||
|
||||
private function buildFunctionDefinitionFromTypeHints(): FunctionLikeDefinition
|
||||
{
|
||||
$parameters = collect($this->reflection->getParameters())
|
||||
->mapWithKeys(fn (ReflectionParameter $p) => [
|
||||
$p->name => ($paramType = $p->getType())
|
||||
? TypeHelper::createTypeFromReflectionType($paramType)
|
||||
: new MixedType,
|
||||
])
|
||||
->all();
|
||||
|
||||
$argumentDefaults = collect($this->reflection->getParameters())
|
||||
->mapWithKeys(fn (ReflectionParameter $p) => [
|
||||
$p->name => rescue(function () use ($p) {
|
||||
return TypeHelper::createTypeFromValue($p->getDefaultValue());
|
||||
}, report: false),
|
||||
])
|
||||
->filter()
|
||||
->all();
|
||||
|
||||
$returnType = ($retType = $this->reflection->getReturnType())
|
||||
? TypeHelper::createTypeFromReflectionType($retType)
|
||||
: new UnknownType;
|
||||
|
||||
$type = new FunctionType($this->name, $parameters, $returnType);
|
||||
|
||||
return new FunctionLikeDefinition(
|
||||
$type,
|
||||
argumentsDefaults: $argumentDefaults,
|
||||
definingClassName: $this->reflection instanceof ReflectionMethod ? $this->reflection->class : null,
|
||||
);
|
||||
}
|
||||
|
||||
private function applyPhpDoc(FunctionLikeDefinition $definition, PhpDocNode $phpDoc): void
|
||||
{
|
||||
foreach ($phpDoc->getTemplateTagValues() as $templateTagValue) {
|
||||
$definition->type->templates[] = new TemplateType(
|
||||
name: $templateTagValue->name,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($phpDoc->getParamTagValues() as $paramTagValue) {
|
||||
$name = Str::replaceFirst('$', '', $paramTagValue->parameterName);
|
||||
if (! array_key_exists($name, $definition->type->arguments)) {
|
||||
continue;
|
||||
}
|
||||
$definition->type->arguments[$name] = $this->handleStatic(
|
||||
PhpDocTypeHelper::toType($paramTagValue->type),
|
||||
$definition->type->templates,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($phpDoc->getThrowsTagValues() as $throwsTagValue) {
|
||||
$definition->type->exceptions[] = $this->handleStatic(
|
||||
PhpDocTypeHelper::toType($throwsTagValue->type),
|
||||
$definition->type->templates,
|
||||
);
|
||||
}
|
||||
|
||||
if ($returnTagValues = array_values($phpDoc->getReturnTagValues())) {
|
||||
$definition->type->returnType = $this->handleStatic(
|
||||
PhpDocTypeHelper::toType($returnTagValues[0]->type),
|
||||
$definition->type->templates,
|
||||
);
|
||||
}
|
||||
|
||||
if (method_exists($phpDoc, 'getSelfOutTypeTagValues')) { // @phpstan-ignore function.alreadyNarrowedType
|
||||
foreach ($phpDoc->getSelfOutTypeTagValues() as $selfOutTypeTagValue) {
|
||||
$selfOutType = $this->handleStatic(
|
||||
PhpDocTypeHelper::toType($selfOutTypeTagValue->type),
|
||||
$definition->type->templates,
|
||||
);
|
||||
|
||||
if (! $selfOutType instanceof Generic) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$definition->selfOutType = $this->handleSelfOutType($selfOutType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TemplateType[] $functionTemplates
|
||||
*/
|
||||
private function handleStatic(Type $type, array $functionTemplates): Type
|
||||
{
|
||||
$functionTemplatesByKeys = collect($functionTemplates)->keyBy->name;
|
||||
|
||||
return (new TypeWalker)->map($type, function (Type $t) use ($functionTemplatesByKeys) {
|
||||
if (! $t instanceof ObjectType) {
|
||||
return $t;
|
||||
}
|
||||
|
||||
$t->name = ltrim($t->name, '\\');
|
||||
|
||||
if ($definedTemplate = $this->classTemplates->get($t->name)) {
|
||||
return $definedTemplate;
|
||||
}
|
||||
|
||||
if ($definedFnTemplate = $functionTemplatesByKeys->get($t->name)) {
|
||||
return $definedFnTemplate;
|
||||
}
|
||||
|
||||
return $t;
|
||||
});
|
||||
}
|
||||
|
||||
private function handleSelfOutType(Generic $type): Generic
|
||||
{
|
||||
return new Generic('self', $type->templateTypes);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user