├── .dockerignore ├── .editorconfig ├── .env.example ├── .env.test ├── .gitignore ├── .phpunit-watcher.yml ├── Makefile ├── autoload.php ├── codeception.yml ├── composer-require-checker.json ├── composer.json ├── config ├── .gitignore ├── common │ ├── di │ │ ├── cache.php │ │ ├── hydrator.php │ │ ├── logger.php │ │ └── router.php │ ├── params.php │ └── routes.php ├── console │ ├── commands.php │ └── params.php ├── environments │ ├── dev │ │ └── params.php │ ├── prod │ │ └── params.php │ └── test │ │ └── params.php └── web │ ├── di │ ├── application.php │ ├── data-response.php │ ├── error-handler.php │ ├── middleware-dispatcher.php │ └── psr17.php │ ├── events.php │ └── params.php ├── configuration.php ├── docker ├── .env ├── Dockerfile ├── compose.dev.yml ├── compose.prod.yml └── compose.yml ├── infection.json.dist ├── phpunit.xml.dist ├── psalm.xml ├── public ├── assets │ └── .gitignore ├── favicon.ico ├── index.php └── robots.txt ├── rector.php ├── runtime └── .gitignore ├── src ├── Command │ └── HelloCommand.php ├── Controller │ └── IndexController.php ├── Exception │ └── ApplicationException.php ├── Http │ ├── ApiResponseData.php │ ├── ApiResponseDataFactory.php │ ├── ApiResponseFormatter.php │ ├── ExceptionMiddleware.php │ ├── NotFoundHandler.php │ └── PaginatorFormatter.php └── Installer.php ├── tests ├── Acceptance.suite.yml ├── Acceptance │ └── SiteCest.php ├── Cli.suite.yml ├── Cli │ ├── ConsoleCest.php │ └── HelloCommandCest.php ├── Functional.suite.yml ├── Functional │ ├── EventListenerConfigurationTest.php │ └── InfoControllerTest.php ├── Support │ ├── AcceptanceTester.php │ ├── ApplicationDataProvider.php │ ├── CliTester.php │ ├── FunctionalTester.php │ ├── Helper │ │ └── Acceptance.php │ ├── UnitTester.php │ └── _generated │ │ └── .gitignore ├── Unit.suite.yml └── Unit │ └── Http │ └── ApiResponseDataFactoryTest.php ├── yii └── yii.bat /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | 4 | # except: 5 | 6 | !/config 7 | !/public 8 | !/src 9 | !/vendor 10 | !autoload.php 11 | !configuration.php 12 | !yii 13 | !composer.json 14 | !composer.lock 15 | !.env* 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | 13 | [*.php] 14 | ij_php_space_before_short_closure_left_parenthesis = false 15 | ij_php_space_after_type_cast = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | 20 | [*.yml] 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | YII_ENV=dev 2 | YII_DEBUG=true 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | YII_ENV=test 2 | YII_DEBUG=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | # phpunit cache 29 | .phpunit.result.cache 30 | 31 | # Codeception 32 | c3.php 33 | 34 | #tests 35 | tests/Support/Data/database.db_snapshot 36 | 37 | /.env 38 | -------------------------------------------------------------------------------- /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | # Run silent. 4 | MAKEFLAGS += --silent 5 | 6 | RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 7 | $(eval $(RUN_ARGS):;@:) 8 | 9 | include docker/.env 10 | 11 | # Current user ID and group ID. 12 | export UID=$(shell id -u) 13 | export GID=$(shell id -g) 14 | 15 | export COMPOSE_PROJECT_NAME=${STACK_NAME} 16 | 17 | up: ## Up the dev environment. 18 | docker compose -f docker/compose.yml -f docker/compose.dev.yml up -d --remove-orphans 19 | 20 | up-build: ## Up the dev environment rebuilding images. 21 | docker compose -f docker/compose.yml -f docker/compose.dev.yml up -d --remove-orphans --build 22 | 23 | down: ## Down the dev environment. 24 | docker compose -f docker/compose.yml -f docker/compose.dev.yml down --remove-orphans 25 | 26 | exec: ## Run a command within the existing container. 27 | docker compose -f docker/compose.yml -f docker/compose.dev.yml exec app $(CMD) $(RUN_ARGS) 28 | 29 | run: ## Run a command within a temporary container. 30 | docker compose -f docker/compose.yml -f docker/compose.dev.yml run --rm --entrypoint $(CMD) app $(RUN_ARGS) 31 | 32 | shell: CMD="/bin/sh" ## Get into container shell. 33 | shell: exec 34 | 35 | yii: CMD="./yii" ## Execute Yii command. 36 | yii: run 37 | 38 | composer: CMD="composer" ## Run Composer. 39 | composer: run 40 | 41 | codecept: CMD="./vendor/bin/codecept" ## Run Codeception. 42 | codecept: run 43 | 44 | psalm: CMD="./vendor/bin/psalm" ## Run Psalm. 45 | psalm: run 46 | 47 | build-prod: ## Build an image. 48 | docker build --file docker/Dockerfile --target prod --pull -t ${IMAGE}:${IMAGE_TAG} . 49 | 50 | push-prod: ## Push image to repository. 51 | docker push ${IMAGE}:${IMAGE_TAG} 52 | 53 | deploy-prod: ## Deploy to production. 54 | docker -H ${PROD_SSH} stack deploy --with-registry-auth -d -c docker/compose.yml -c docker/compose.prod.yml ${STACK_NAME} 55 | 56 | # Output the help for each task, see https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 57 | .PHONY: help 58 | help: ## This help. 59 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 60 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | load(); 11 | 12 | $_ENV['YII_ENV'] = empty($_ENV['YII_ENV']) ? null : $_ENV['YII_ENV']; 13 | $_SERVER['YII_ENV'] = $_ENV['YII_ENV']; 14 | 15 | $_ENV['YII_DEBUG'] = filter_var( 16 | !empty($_ENV['YII_DEBUG']) ? $_ENV['YII_DEBUG'] : true, 17 | FILTER_VALIDATE_BOOLEAN, 18 | FILTER_NULL_ON_FAILURE 19 | ) ?? true; 20 | $_SERVER['YII_DEBUG'] = $_ENV['YII_DEBUG']; 21 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | namespace: App\Tests 2 | support_namespace: Support 3 | paths: 4 | tests: tests 5 | output: runtime/tests/_output 6 | data: tests/Support/Data 7 | support: tests/Support 8 | envs: tests/_envs 9 | actor_suffix: Tester 10 | extensions: 11 | enabled: 12 | - Codeception\Extension\RunFailed 13 | settings: 14 | suite_class: \PHPUnit_Framework_TestSuite 15 | memory_limit: 1024M 16 | colors: true 17 | coverage: 18 | enabled: true 19 | whitelist: 20 | include: 21 | - src/* 22 | exclude: 23 | - src/Asset/* 24 | - src/Installer.php 25 | params: 26 | - .env 27 | - .env.test 28 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "Composer\\Script\\Event" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/app-api", 3 | "type": "project", 4 | "description": "Yii Framework API project template", 5 | "keywords": [ 6 | "yii3", 7 | "app", 8 | "api", 9 | "rest" 10 | ], 11 | "homepage": "https://github.com/yiisoft/app-api/", 12 | "license": "BSD-3-Clause", 13 | "support": { 14 | "issues": "https://github.com/yiisoft/app-api/issues?state=open", 15 | "source": "https://github.com/yiisoft/app-api", 16 | "forum": "https://www.yiiframework.com/forum/", 17 | "wiki": "https://www.yiiframework.com/wiki/", 18 | "irc": "ircs://irc.libera.chat:6697/yii", 19 | "chat": "https://t.me/yii3en" 20 | }, 21 | "funding": [ 22 | { 23 | "type": "opencollective", 24 | "url": "https://opencollective.com/yiisoft" 25 | }, 26 | { 27 | "type": "github", 28 | "url": "https://github.com/sponsors/yiisoft" 29 | } 30 | ], 31 | "minimum-stability": "dev", 32 | "prefer-stable": true, 33 | "require": { 34 | "php": "^8.1", 35 | "cebe/markdown": "^1.2@dev", 36 | "httpsoft/http-message": "^1.1", 37 | "psr/container": "^2.0", 38 | "psr/http-factory": "^1.0", 39 | "psr/http-message": "^1.1|^2.0", 40 | "psr/http-server-handler": "^1.0", 41 | "psr/http-server-middleware": "^1.0", 42 | "psr/log": "^3.0", 43 | "symfony/console": "^6.0", 44 | "vlucas/phpdotenv": "^5.3", 45 | "yiisoft/config": "^1.0", 46 | "yiisoft/data": "^1.0", 47 | "yiisoft/data-response": "^2.0", 48 | "yiisoft/definitions": "^3.0", 49 | "yiisoft/di": "^1.2", 50 | "yiisoft/error-handler": "^3.0", 51 | "yiisoft/factory": "^1.1", 52 | "yiisoft/files": "^2.0", 53 | "yiisoft/http": "^1.2", 54 | "yiisoft/hydrator": "^1.0", 55 | "yiisoft/hydrator-validator": "^2.0", 56 | "yiisoft/injector": "^1.1", 57 | "yiisoft/input-http": "^1.0", 58 | "yiisoft/log": "^2.0", 59 | "yiisoft/log-target-file": "^3.0", 60 | "yiisoft/middleware-dispatcher": "^5.2", 61 | "yiisoft/request-body-parser": "^1.1", 62 | "yiisoft/router": "^3.0", 63 | "yiisoft/router-fastroute": "^3.0", 64 | "yiisoft/validator": "^1.0", 65 | "yiisoft/cache-file": "^3.0", 66 | "yiisoft/yii-console": "^2.0", 67 | "yiisoft/yii-debug": "dev-master", 68 | "yiisoft/yii-event": "^2.0", 69 | "yiisoft/yii-http": "^1.0", 70 | "yiisoft/yii-middleware": "^1.0", 71 | "yiisoft/yii-runner-console": "^2.0", 72 | "yiisoft/yii-runner-http": "^2.0", 73 | "yiisoft/yii-swagger": "^2.0" 74 | }, 75 | "require-dev": { 76 | "codeception/c3": "^2.6", 77 | "codeception/codeception": "^5.0", 78 | "codeception/lib-innerbrowser": "^3.0", 79 | "codeception/module-asserts": "^3.0", 80 | "codeception/module-cli": "^2.0", 81 | "codeception/module-db": "^3.0", 82 | "codeception/module-phpbrowser": "^3.0", 83 | "codeception/module-rest": "^3.0", 84 | "maglnet/composer-require-checker": "^4.7", 85 | "phpunit/phpunit": "^9.5", 86 | "roave/infection-static-analysis-plugin": "^1.34", 87 | "spatie/phpunit-watcher": "^1.23", 88 | "rector/rector": "^1.0.0", 89 | "vimeo/psalm": "^5.20", 90 | "yiisoft/json": "^1.0", 91 | "yiisoft/yii-debug-api": "dev-master", 92 | "yiisoft/yii-debug-viewer": "dev-master", 93 | "yiisoft/yii-testing": "dev-master" 94 | }, 95 | "autoload": { 96 | "psr-4": { 97 | "App\\": "src" 98 | } 99 | }, 100 | "autoload-dev": { 101 | "psr-4": { 102 | "App\\Tests\\": "tests/" 103 | } 104 | }, 105 | "scripts": { 106 | "serve": [ 107 | "Composer\\Config::disableProcessTimeout", 108 | "@php ./yii serve" 109 | ], 110 | "post-update-cmd": [ 111 | "App\\Installer::postUpdate", 112 | "App\\Installer::copyEnvFile" 113 | ], 114 | "post-create-project-cmd": [ 115 | "App\\Installer::copyEnvFile" 116 | ], 117 | "test": "codecept run", 118 | "test-watch": "phpunit-watcher watch" 119 | }, 120 | "extra": { 121 | "branch-alias": { 122 | "dev-master": "1.0.x-dev" 123 | }, 124 | "config-plugin-file": "configuration.php" 125 | }, 126 | "config": { 127 | "sort-packages": true, 128 | "allow-plugins": { 129 | "codeception/c3": true, 130 | "infection/extension-installer": true, 131 | "composer/package-versions-deprecated": true, 132 | "yiisoft/config": true 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | *-local.php 2 | .merge-plan.php 3 | -------------------------------------------------------------------------------- /config/common/di/cache.php: -------------------------------------------------------------------------------- 1 | Yiisoft\Cache\Cache::class, 7 | Psr\SimpleCache\CacheInterface::class => Yiisoft\Cache\File\FileCache::class, 8 | ]; 9 | -------------------------------------------------------------------------------- /config/common/di/hydrator.php: -------------------------------------------------------------------------------- 1 | ContainerAttributeResolverFactory::class, 12 | ObjectFactoryInterface::class => ContainerObjectFactory::class, 13 | ]; 14 | -------------------------------------------------------------------------------- /config/common/di/logger.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'class' => Logger::class, 16 | '__construct()' => [ 17 | 'targets' => ReferencesArray::from([ 18 | FileTarget::class, 19 | StreamTarget::class, 20 | ]), 21 | ], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /config/common/di/router.php: -------------------------------------------------------------------------------- 1 | static function (RouteCollectorInterface $collector) use ($config) { 18 | $collector 19 | ->middleware(RequestBodyParser::class) 20 | ->middleware(FormatDataResponse::class) 21 | ->middleware(ExceptionMiddleware::class) 22 | ->addGroup( 23 | Group::create()->routes(...$config->get('routes')), 24 | ); 25 | 26 | return new RouteCollection($collector); 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /config/common/params.php: -------------------------------------------------------------------------------- 1 | 'support@example.com', 10 | 11 | 'yiisoft/aliases' => [ 12 | 'aliases' => [ 13 | '@root' => dirname(__DIR__, 2), 14 | '@assets' => '@public/assets', 15 | '@assetsUrl' => '@baseUrl/assets', 16 | '@baseUrl' => '/', 17 | '@data' => '@root/data', 18 | '@messages' => '@resources/messages', 19 | '@public' => '@root/public', 20 | '@resources' => '@root/resources', 21 | '@runtime' => '@root/runtime', 22 | '@src' => '@root/src', 23 | '@tests' => '@root/tests', 24 | '@views' => '@root/views', 25 | '@vendor' => '@root/vendor', 26 | ], 27 | ], 28 | 29 | 'yiisoft/router-fastroute' => [ 30 | 'enableCache' => false, 31 | ], 32 | 33 | 'yiisoft/view' => [ 34 | 'basePath' => '@views', 35 | 'parameters' => [ 36 | 'assetManager' => Reference::to(AssetManager::class), 37 | ], 38 | ], 39 | 40 | 'yiisoft/yii-swagger' => [ 41 | 'annotation-paths' => [ 42 | '@src', 43 | ], 44 | ], 45 | ]; 46 | -------------------------------------------------------------------------------- /config/common/routes.php: -------------------------------------------------------------------------------- 1 | action([IndexController::class, 'index']) 17 | ->name('app/index'), 18 | 19 | Group::create('/docs') 20 | ->routes( 21 | Route::get('') 22 | ->middleware(FormatDataResponseAsHtml::class) 23 | ->action(fn (SwaggerUi $swaggerUi) => $swaggerUi->withJsonUrl('/docs/openapi.json')), 24 | Route::get('/openapi.json') 25 | ->middleware(FormatDataResponseAsJson::class) 26 | ->middleware(CorsAllowAll::class) 27 | ->action([SwaggerJson::class, 'process']), 28 | ), 29 | ]; 30 | -------------------------------------------------------------------------------- /config/console/commands.php: -------------------------------------------------------------------------------- 1 | HelloCommand::class, 9 | ]; 10 | -------------------------------------------------------------------------------- /config/console/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'commands' => require __DIR__ . '/commands.php', 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /config/environments/dev/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'enabled' => true, 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /config/environments/prod/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'enabled' => false, 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /config/environments/test/params.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'enabled' => false, 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /config/web/di/application.php: -------------------------------------------------------------------------------- 1 | [ 15 | '__construct()' => [ 16 | 'dispatcher' => DynamicReference::to(static function (Injector $injector) use ($params) { 17 | return $injector->make(MiddlewareDispatcher::class) 18 | ->withMiddlewares($params['middlewares']); 19 | }), 20 | 'fallbackHandler' => Reference::to(NotFoundHandler::class), 21 | ], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /config/web/di/data-response.php: -------------------------------------------------------------------------------- 1 | ApiResponseFormatter::class, 18 | DataResponseFactoryInterface::class => DataResponseFactory::class, 19 | ContentNegotiator::class => [ 20 | '__construct()' => [ 21 | 'contentFormatters' => [ 22 | 'text/html' => new HtmlDataResponseFormatter(), 23 | 'application/xml' => new XmlDataResponseFormatter(), 24 | 'application/json' => new JsonDataResponseFormatter(), 25 | ], 26 | ], 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /config/web/di/error-handler.php: -------------------------------------------------------------------------------- 1 | JsonRenderer::class, 14 | ]; 15 | -------------------------------------------------------------------------------- /config/web/di/middleware-dispatcher.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'class' => CompositeParametersResolver::class, 18 | '__construct()' => [ 19 | Reference::to(HydratorAttributeParametersResolver::class), 20 | Reference::to(RequestInputParametersResolver::class), 21 | ], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /config/web/di/psr17.php: -------------------------------------------------------------------------------- 1 | RequestFactory::class, 20 | ServerRequestFactoryInterface::class => ServerRequestFactory::class, 21 | ResponseFactoryInterface::class => ResponseFactory::class, 22 | StreamFactoryInterface::class => StreamFactory::class, 23 | UriFactoryInterface::class => UriFactory::class, 24 | UploadedFileFactoryInterface::class => UploadedFileFactory::class, 25 | ]; 26 | -------------------------------------------------------------------------------- /config/web/events.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'requestInputParametersResolver' => [ 12 | 'throwInputValidationException' => true, 13 | ], 14 | ], 15 | 16 | 'middlewares' => [ 17 | ErrorCatcher::class, 18 | Subfolder::class, 19 | Router::class, 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /configuration.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'params' => 'common/params.php', 8 | 'params-web' => [ 9 | '$params', 10 | 'web/params.php', 11 | ], 12 | 'params-console' => [ 13 | '$params', 14 | 'console/params.php', 15 | ], 16 | 'di' => 'common/di/*.php', 17 | 'di-web' => [ 18 | '$di', 19 | 'web/di/*.php', 20 | ], 21 | 'di-console' => '$di', 22 | 'di-delegates' => [], 23 | 'di-delegates-console' => '$di-delegates', 24 | 'di-delegates-web' => '$di-delegates', 25 | 'di-providers' => [], 26 | 'di-providers-console' => '$di-providers', 27 | 'di-providers-web' => '$di-providers', 28 | 'events' => [], 29 | 'events-console' => '$events', 30 | 'events-web' => [ 31 | '$events', 32 | 'web/events.php', 33 | ], 34 | 'bootstrap' => [], 35 | 'bootstrap-console' => '$bootstrap', 36 | 'bootstrap-web' => '$bootstrap', 37 | 'routes' => [ 38 | 'common/routes.php', 39 | ], 40 | ], 41 | 'config-plugin-environments' => [ 42 | 'dev' => [ 43 | 'params' => [ 44 | 'environments/dev/params.php', 45 | ], 46 | ], 47 | 'prod' => [ 48 | 'params' => [ 49 | 'environments/prod/params.php', 50 | ], 51 | ], 52 | 'test' => [ 53 | 'params' => [ 54 | 'environments/test/params.php', 55 | ], 56 | ], 57 | ], 58 | 'config-plugin-options' => [ 59 | 'source-directory' => 'config', 60 | ], 61 | ]; 62 | -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | STACK_NAME=app-api 2 | 3 | PROD_HOST=app-api.example.com 4 | PROD_SSH="ssh://docker-web" 5 | 6 | IMAGE=app-api 7 | IMAGE_TAG=latest 8 | 9 | DEV_PORT=80 10 | 11 | # XDEBUG_MODE=develop,debug 12 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM composer/composer:latest-bin AS composer 2 | 3 | FROM ghcr.io/shyim/wolfi-php/frankenphp:8.3 AS base 4 | ARG UID=10001 5 | ARG GID=10001 6 | RUN < 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ./tests 23 | 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/assets/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiisoft/app-api/ecef1f18685e9b4c571eb0d970f26df880434847/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | withTemporaryErrorHandler(new ErrorHandler( 51 | new Logger([new FileTarget(dirname(__DIR__) . '/runtime/logs/app.log')]), 52 | new JsonRenderer(), 53 | )) 54 | ; 55 | $runner->run(); 56 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 14 | __DIR__ . '/src', 15 | __DIR__ . '/tests', 16 | ]); 17 | 18 | // register a single rule 19 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 20 | 21 | // define sets of rules 22 | $rectorConfig->sets([ 23 | LevelSetList::UP_TO_PHP_81, 24 | ]); 25 | 26 | $rectorConfig->skip([ 27 | ClosureToArrowFunctionRector::class, 28 | JsonThrowOnErrorRector::class, 29 | ReadOnlyPropertyRector::class, 30 | ]); 31 | }; 32 | -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/Command/HelloCommand.php: -------------------------------------------------------------------------------- 1 | writeln('Hello!'); 19 | return ExitCode::OK; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | createResponse(['version' => '3.0', 'author' => 'yiisoft']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/ApplicationException.php: -------------------------------------------------------------------------------- 1 | status; 59 | } 60 | 61 | public function setStatus(string $status): self 62 | { 63 | $this->status = $status; 64 | return $this; 65 | } 66 | 67 | public function getErrorMessage(): string 68 | { 69 | return $this->errorMessage; 70 | } 71 | 72 | public function setErrorMessage(string $errorMessage): self 73 | { 74 | $this->errorMessage = $errorMessage; 75 | return $this; 76 | } 77 | 78 | public function getErrorCode(): ?int 79 | { 80 | return $this->errorCode; 81 | } 82 | 83 | public function setErrorCode(int $errorCode): self 84 | { 85 | $this->errorCode = $errorCode; 86 | return $this; 87 | } 88 | 89 | public function getData(): ?array 90 | { 91 | return $this->data; 92 | } 93 | 94 | public function setData(?array $data): self 95 | { 96 | $this->data = $data; 97 | return $this; 98 | } 99 | 100 | public function toArray(): array 101 | { 102 | return [ 103 | 'status' => $this->getStatus(), 104 | 'error_message' => $this->getErrorMessage(), 105 | 'error_code' => $this->getErrorCode(), 106 | 'data' => $this->getData(), 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Http/ApiResponseDataFactory.php: -------------------------------------------------------------------------------- 1 | getStatusCode() !== Status::OK) { 16 | return $this 17 | ->createErrorResponse() 18 | ->setErrorCode($response->getStatusCode()) 19 | ->setErrorMessage($this->getErrorMessage($response)); 20 | } 21 | 22 | $data = $response->getData(); 23 | 24 | if ($data !== null && !is_array($data)) { 25 | throw new RuntimeException('The response data must be either null or an array'); 26 | } 27 | 28 | return $this 29 | ->createSuccessResponse() 30 | ->setData($data); 31 | } 32 | 33 | public function createSuccessResponse(): ApiResponseData 34 | { 35 | return $this 36 | ->createResponse() 37 | ->setStatus('success'); 38 | } 39 | 40 | public function createErrorResponse(): ApiResponseData 41 | { 42 | return $this 43 | ->createResponse() 44 | ->setStatus('failed'); 45 | } 46 | 47 | public function createResponse(): ApiResponseData 48 | { 49 | return new ApiResponseData(); 50 | } 51 | 52 | private function getErrorMessage(DataResponse $response): string 53 | { 54 | $data = $response->getData(); 55 | if (is_string($data) && !empty($data)) { 56 | return $data; 57 | } 58 | 59 | return 'Unknown error'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Http/ApiResponseFormatter.php: -------------------------------------------------------------------------------- 1 | withData( 21 | $this->apiResponseDataFactory 22 | ->createFromResponse($dataResponse) 23 | ->toArray(), 24 | ); 25 | 26 | return $this->jsonDataResponseFormatter->format($response); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/ExceptionMiddleware.php: -------------------------------------------------------------------------------- 1 | handle($request); 26 | } catch (ApplicationException $e) { 27 | return $this->dataResponseFactory->createResponse($e->getMessage(), $e->getCode()); 28 | } catch (InputValidationException $e) { 29 | return $this->dataResponseFactory->createResponse( 30 | $e->getResult()->getErrorMessages()[0], 31 | Status::BAD_REQUEST 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/NotFoundHandler.php: -------------------------------------------------------------------------------- 1 | formatter->format( 25 | $this->dataResponseFactory->createResponse( 26 | 'Not found.', 27 | Status::NOT_FOUND, 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http/PaginatorFormatter.php: -------------------------------------------------------------------------------- 1 | $paginator->getPageSize(), 25 | 'currentPage' => $paginator->getCurrentPage(), 26 | 'totalPages' => $paginator->getTotalPages(), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Installer.php: -------------------------------------------------------------------------------- 1 | $iterator */ 28 | $iterator = new RIterator( 29 | new DirIterator($path, FSIterator::SKIP_DOTS | FSIterator::CURRENT_AS_PATHNAME), 30 | RIterator::SELF_FIRST 31 | ); 32 | 33 | foreach ($iterator as $item) { 34 | chmod($item, $mode); 35 | } 36 | } 37 | 38 | public static function copyEnvFile(): void 39 | { 40 | if (!file_exists('.env')) { 41 | copy('.env.example', '.env'); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | actor: AcceptanceTester 2 | extensions: 3 | enabled: 4 | - Codeception\Extension\RunProcess: 5 | 0: composer serve 6 | sleep: 3 7 | modules: 8 | enabled: 9 | - REST: 10 | url: http://127.0.0.1:8080 11 | depends: PhpBrowser 12 | - App\Tests\Support\Helper\Acceptance 13 | step_decorators: ~ 14 | -------------------------------------------------------------------------------- /tests/Acceptance/SiteCest.php: -------------------------------------------------------------------------------- 1 | sendGET('/'); 15 | $I->seeResponseCodeIs(HttpCode::OK); 16 | $I->seeResponseIsJson(); 17 | $I->seeResponseContainsJson( 18 | [ 19 | 'status' => 'success', 20 | 'error_message' => '', 21 | 'error_code' => null, 22 | 'data' => [ 23 | 'version' => '3.0', 24 | 'author' => 'yiisoft', 25 | ], 26 | ] 27 | ); 28 | } 29 | 30 | public function testNotFoundPage(AcceptanceTester $I): void 31 | { 32 | $I->sendGET('/not_found_page'); 33 | $I->seeResponseCodeIs(HttpCode::NOT_FOUND); 34 | $I->seeResponseIsJson(); 35 | $I->seeResponseContainsJson( 36 | [ 37 | 'status' => 'failed', 38 | 'error_message' => 'Not found.', 39 | 'error_code' => 404, 40 | 'data' => null, 41 | ] 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Cli.suite.yml: -------------------------------------------------------------------------------- 1 | actor: CliTester 2 | modules: 3 | enabled: 4 | - Cli 5 | - Asserts 6 | step_decorators: ~ 7 | -------------------------------------------------------------------------------- /tests/Cli/ConsoleCest.php: -------------------------------------------------------------------------------- 1 | runShellCommand($command); 15 | $I->seeInShellOutput('Yii Console'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Cli/HelloCommandCest.php: -------------------------------------------------------------------------------- 1 | get('params-console'); 20 | 21 | $loader = new ContainerCommandLoader( 22 | ApplicationDataProvider::getConsoleContainer(), 23 | $params['yiisoft/yii-console']['commands'] 24 | ); 25 | 26 | $app->setCommandLoader($loader); 27 | 28 | $command = $app->find('hello'); 29 | 30 | $commandCreate = new CommandTester($command); 31 | 32 | $commandCreate->setInputs(['yes']); 33 | 34 | $I->assertSame(ExitCode::OK, $commandCreate->execute([])); 35 | 36 | $output = $commandCreate->getDisplay(true); 37 | 38 | $I->assertStringContainsString('Hello!', $output); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Functional.suite.yml: -------------------------------------------------------------------------------- 1 | actor: FunctionalTester 2 | modules: 3 | enabled: 4 | step_decorators: ~ 5 | -------------------------------------------------------------------------------- /tests/Functional/EventListenerConfigurationTest.php: -------------------------------------------------------------------------------- 1 | get(ListenerConfigurationChecker::class); 19 | $checker->check($config->get('events-console')); 20 | 21 | self::assertInstanceOf(ListenerConfigurationChecker::class, $checker); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Functional/InfoControllerTest.php: -------------------------------------------------------------------------------- 1 | tester = new FunctionalTester(); 17 | } 18 | 19 | public function testGetIndex() 20 | { 21 | $method = 'GET'; 22 | $url = '/'; 23 | 24 | $this->tester->bootstrapApplication(dirname(__DIR__, 2)); 25 | $response = $this->tester->doRequest($method, $url); 26 | 27 | $this->assertEquals( 28 | [ 29 | 'status' => 'success', 30 | 'error_message' => '', 31 | 'error_code' => null, 32 | 'data' => ['version' => '3.0', 'author' => 'yiisoft'], 33 | ], 34 | $response->getContentAsJson() 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | getConfig(); 20 | } 21 | 22 | public static function getConsoleContainer(): ContainerInterface 23 | { 24 | return self::getConsoleRunner()->getContainer(); 25 | } 26 | 27 | public static function getWebConfig(): Config 28 | { 29 | return self::getConsoleRunner()->getConfig(); 30 | } 31 | 32 | public static function getWebContainer(): ContainerInterface 33 | { 34 | return self::getConsoleRunner()->getContainer(); 35 | } 36 | 37 | private static function getConsoleRunner(): ConsoleApplicationRunner 38 | { 39 | if (self::$consoleRunner === null) { 40 | self::$consoleRunner = new ConsoleApplicationRunner( 41 | rootPath: dirname(__DIR__, 2), 42 | environment: $_ENV['YII_ENV'] 43 | ); 44 | } 45 | return self::$consoleRunner; 46 | } 47 | 48 | private static function getWebRunner(): HttpApplicationRunner 49 | { 50 | if (self::$webRunner === null) { 51 | self::$webRunner = new HttpApplicationRunner( 52 | rootPath: dirname(__DIR__, 2), 53 | environment: $_ENV['YII_ENV'] 54 | ); 55 | } 56 | return self::$webRunner; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Support/CliTester.php: -------------------------------------------------------------------------------- 1 | createResponse(); 21 | 22 | $this->assertEquals([ 23 | 'status' => '', 24 | 'error_message' => '', 25 | 'error_code' => null, 26 | 'data' => null, 27 | ], $response->toArray()); 28 | } 29 | 30 | public function testSuccessResponse(): void 31 | { 32 | $factory = new ApiResponseDataFactory(); 33 | 34 | $response = $factory->createSuccessResponse(); 35 | 36 | $this->assertEquals([ 37 | 'status' => 'success', 38 | 'error_message' => '', 39 | 'error_code' => null, 40 | 'data' => null, 41 | ], $response->toArray()); 42 | } 43 | 44 | public function testErrorResponse(): void 45 | { 46 | $factory = new ApiResponseDataFactory(); 47 | 48 | $response = $factory->createErrorResponse(); 49 | 50 | $this->assertEquals([ 51 | 'status' => 'failed', 52 | 'error_message' => '', 53 | 'error_code' => null, 54 | 'data' => null, 55 | ], $response->toArray()); 56 | } 57 | 58 | public function testGenericResponse(): void 59 | { 60 | $factory = new ApiResponseDataFactory(); 61 | 62 | $response = new DataResponse( 63 | 'error message', 64 | 555, 65 | 'Testing phase', 66 | new ResponseFactory(), 67 | $this->createStub(StreamFactoryInterface::class), 68 | ); 69 | $response = $factory->createFromResponse($response); 70 | 71 | $this->assertEquals([ 72 | 'status' => 'failed', 73 | 'error_message' => 'error message', 74 | 'error_code' => 555, 75 | 'data' => null, 76 | ], $response->toArray()); 77 | } 78 | 79 | public function testGenericSuccessResponse(): void 80 | { 81 | $factory = new ApiResponseDataFactory(); 82 | 83 | $response = new DataResponse( 84 | ['message' => 'success message'], 85 | 200, 86 | 'Testing phase', 87 | new ResponseFactory(), 88 | $this->createStub(StreamFactoryInterface::class), 89 | ); 90 | $response = $factory->createFromResponse($response); 91 | 92 | $this->assertEquals([ 93 | 'status' => 'success', 94 | 'error_message' => '', 95 | 'error_code' => null, 96 | 'data' => ['message' => 'success message'], 97 | ], $response->toArray()); 98 | } 99 | 100 | public function testUnableToUseNotArrayDataInGenericResponse(): void 101 | { 102 | $factory = new ApiResponseDataFactory(); 103 | 104 | $response = new DataResponse( 105 | 'success message', 106 | 200, 107 | 'Testing phase', 108 | new ResponseFactory(), 109 | $this->createStub(StreamFactoryInterface::class), 110 | ); 111 | 112 | $this->expectException(RuntimeException::class); 113 | $this->expectExceptionMessage('The response data must be either null or an array'); 114 | 115 | $factory->createFromResponse($response); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /yii: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 18 | -------------------------------------------------------------------------------- /yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | @setlocal 3 | set YII_PATH=%~dp0 4 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php 5 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 6 | @endlocal 7 | --------------------------------------------------------------------------------