├── .github ├── ISSUE_TEMPLATE │ ├── feature.md │ ├── question.md │ └── bug_report.md ├── SECURITY.md ├── workflows │ ├── coding-standards.yml │ ├── static-analysis.yml │ ├── continuous-integration.yml │ ├── update-copyright-years-in-license-file.yml │ ├── claude-code-review.yml │ └── claude.yml ├── dependabot.yml └── CONTRIBUTING.md ├── src ├── Exception │ ├── ExceptionInterface.php │ ├── InvalidContextException.php │ ├── InvalidModuleException.php │ ├── LogicException.php │ ├── RuntimeException.php │ ├── LocationHeaderRequestException.php │ ├── RouterException.php │ ├── DirectoryNotWritableException.php │ └── InvalidRequestJsonException.php ├── Annotation │ ├── ReturnCreatedResource.php │ └── StdIn.php ├── Provide │ ├── Error │ │ ├── ErrorPageFactoryInterface.php │ │ ├── DevVndErrorPageFactory.php │ │ ├── ProdVndErrorPageFactory.php │ │ ├── VndErrorModule.php │ │ ├── NullPage.php │ │ ├── Status.php │ │ ├── ErrorHandler.php │ │ ├── ErrorLogger.php │ │ ├── LogRef.php │ │ ├── ExceptionAsString.php │ │ ├── ProdVndErrorPage.php │ │ └── DevVndErrorPage.php │ ├── Logger │ │ ├── PsrLoggerModule.php │ │ ├── ProdMonologProvider.php │ │ └── MonologProvider.php │ ├── Router │ │ ├── WebRouterInterface.php │ │ ├── CliRouterHelp.php │ │ ├── WebRouterModule.php │ │ ├── RouterCollectionProvider.php │ │ ├── HttpMethodParamsInterface.php │ │ ├── WebRouter.php │ │ ├── RouterCollection.php │ │ ├── HttpMethodParams.php │ │ └── CliRouter.php │ ├── Representation │ │ ├── CreatedResourceModule.php │ │ ├── RouterReverseLinker.php │ │ ├── CreatedResourceInterceptor.php │ │ └── CreatedResourceRenderer.php │ └── Transfer │ │ └── CliResponder.php ├── AbstractAppModule.php ├── Context │ ├── ApiModule.php │ ├── HalModule.php │ ├── CliModule.php │ └── ProdModule.php ├── Compiler │ ├── FilePutContents.php │ ├── CompileObjectGraph.php │ ├── Bootstrap.php │ ├── CompileClassMetaInfo.php │ ├── FakeRun.php │ ├── CompilePreload.php │ └── CompileAutoload.php ├── Module │ ├── Psr6NullModule.php │ ├── Import │ │ └── ImportApp.php │ ├── ResourceObjectModule.php │ ├── AppMetaModule.php │ ├── ImportSchemeCollectionProvider.php │ └── ImportAppModule.php ├── PackageModule.php ├── Types.php ├── Injector.php ├── Module.php ├── Injector │ ├── FileUpdate.php │ └── PackageInjector.php └── Compiler.php ├── tests-files ├── hash.php └── deleteFiles.php ├── src-deprecated ├── AppMetaModule.php ├── ErrorPage.php ├── Module │ ├── CacheModule.php │ ├── CacheNamespaceModule.php │ └── ScriptInjectorModule.php ├── LazyModule.php ├── Context │ └── Provider │ │ └── ProdCacheProvider.php ├── Provide │ ├── Cache │ │ └── CacheDirProvider.php │ └── Representation │ │ └── RouterReverseLink.php ├── Compiler │ ├── CompileDependencies.php │ ├── CompileDiScripts.php │ ├── NewInstance.php │ └── CompileApp.php ├── Unlink.php ├── Bootstrap.php └── AppInjector.php ├── composer-require-checker.json ├── bin ├── bear.compile.php └── bear.compile ├── tests-deprecated ├── ErrorPageTest.php ├── ContextProviderCompileTest.php ├── AppInjectorTest.php └── BootstrapTest.php ├── LICENSE ├── README.md ├── composer.json └── CLAUDE.md /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: Suggest a new feature or enhancement 4 | labels: Feature 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /tests-files/hash.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | ### How to reproduce 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Provide/Error/ErrorPageFactoryInterface.php: -------------------------------------------------------------------------------- 1 | dumpAutoload() : $compiler->compile(); 16 | exit($code); 17 | -------------------------------------------------------------------------------- /src/Provide/Error/ProdVndErrorPageFactory.php: -------------------------------------------------------------------------------- 1 | bind(LoggerInterface::class)->toProvider(MonologProvider::class)->in(Scope::SINGLETON); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Provide/Error/VndErrorModule.php: -------------------------------------------------------------------------------- 1 | bind(ErrorLogger::class); 17 | $this->bind(ErrorInterface::class)->to(ErrorHandler::class); 18 | $this->bind(ErrorPageFactoryInterface::class)->to(DevVndErrorPageFactory::class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src-deprecated/ErrorPage.php: -------------------------------------------------------------------------------- 1 | postBody = $postBody; 22 | } 23 | 24 | public function __toString() 25 | { 26 | $string = parent::__toString(); 27 | 28 | return $string . $this->postBody; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Provide/Router/WebRouterInterface.php: -------------------------------------------------------------------------------- 1 | bind()->annotatedWith(DefaultSchemeHost::class)->toInstance('app://self'); 21 | $this->bind()->annotatedWith(ContextScheme::class)->toInstance('app://self'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Provide/Router/CliRouterHelp.php: -------------------------------------------------------------------------------- 1 | setSummary('CLI Router'); 17 | $this->setUsage(' '); 18 | $this->setDescr("E.g. \"get /\", \"options /users\", \"post 'app://self/users?name=Sunday'\""); 19 | $this->descr = ''; 20 | $this->summary = ''; 21 | $this->usage = ''; 22 | 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Provide/Router/WebRouterModule.php: -------------------------------------------------------------------------------- 1 | bind(RouterInterface::class)->to(WebRouter::class); 20 | $this->bind(WebRouterInterface::class)->to(WebRouter::class); 21 | $this->bind(HttpMethodParamsInterface::class)->to(HttpMethodParams::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests-deprecated/ErrorPageTest.php: -------------------------------------------------------------------------------- 1 | errorPage = new ErrorPage('some_text_after_error_message'); 20 | } 21 | 22 | public function testToString() 23 | { 24 | $text = (string) $this->errorPage; 25 | $this->assertStringContainsString('some_text_after_error_message', $text); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Provide/Logger/ProdMonologProvider.php: -------------------------------------------------------------------------------- 1 | */ 14 | final class ProdMonologProvider implements ProviderInterface 15 | { 16 | public function __construct( 17 | private AbstractAppMeta $appMeta, 18 | ) { 19 | } 20 | 21 | #[Override] 22 | public function get(): Logger 23 | { 24 | return new Logger($this->appMeta->name, [new ErrorLogHandler()]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src-deprecated/Module/CacheModule.php: -------------------------------------------------------------------------------- 1 | bind(Cache::class)->to(ArrayCache::class); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Provide/Representation/CreatedResourceModule.php: -------------------------------------------------------------------------------- 1 | bind(CreatedResourceRenderer::class); 20 | $this->bindInterceptor( 21 | $this->matcher->any(), 22 | $this->matcher->annotatedWith(ReturnCreatedResource::class), 23 | [CreatedResourceInterceptor::class], 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Alerts only major updates for Packagist (Composer) 2 | # 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "composer" # Specify the correct package ecosystem for PHP 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | ignore: 13 | - dependency-name: "*" # Ignore all dependencies for specific update types 14 | update-types: ["version-update:semver-minor", "version-update:semver-patch"] 15 | - dependency-name: "phpunit/phpunit" 16 | versions: ["*"] 17 | -------------------------------------------------------------------------------- /src-deprecated/Module/CacheNamespaceModule.php: -------------------------------------------------------------------------------- 1 | cacheNamespace = $cacheNamespace; 20 | parent::__construct($module); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function configure(): void 27 | { 28 | $this->bind()->annotatedWith('cache_namespace')->toInstance($this->cacheNamespace); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src-deprecated/Module/ScriptInjectorModule.php: -------------------------------------------------------------------------------- 1 | bind(InjectorInterface::class)->toInstance(new ScriptInjector($this->scriptDir)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Provide/Router/RouterCollectionProvider.php: -------------------------------------------------------------------------------- 1 | */ 13 | final class RouterCollectionProvider implements ProviderInterface 14 | { 15 | public function __construct( 16 | #[Named('primary_router')] 17 | private RouterInterface $primaryRouter, 18 | private WebRouterInterface $webRouter, 19 | ) { 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | #[Override] 26 | public function get(): RouterCollection 27 | { 28 | return new RouterCollection([$this->primaryRouter, $this->webRouter]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Provide/Error/NullPage.php: -------------------------------------------------------------------------------- 1 | renderer = new NullRenderer(); 23 | 24 | return $this; 25 | } 26 | 27 | public function onGet(string $required, int $optional = 0): ResourceObject 28 | { 29 | unset($required, $optional); 30 | 31 | return $this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Compiler/FilePutContents.php: -------------------------------------------------------------------------------- 1 | overwritten[] = $fileName; 27 | } 28 | 29 | file_put_contents($fileName, $content); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | ## Code Contributions 4 | 5 | ## Installation 6 | 7 | Install project dependencies and test tools by running the following commands. 8 | 9 | ```bash 10 | $ composer install 11 | ``` 12 | 13 | ## Running tests 14 | 15 | ```bash 16 | $ composer test 17 | ``` 18 | ```bash 19 | $ composer coverage // xdebug 20 | $ composer pcov // pcov 21 | ``` 22 | 23 | Add tests for your new code ensuring that you have 100% code coverage. 24 | In rare cases, code may be excluded from test coverage using `@codeCoverageIgnore`. 25 | 26 | ## Sending a pull request 27 | 28 | To ensure your PHP code changes pass the CI checks, make sure to run all the same checks before submitting a PR. 29 | 30 | ```bash 31 | $ composer tests 32 | ``` 33 | 34 | When you make a pull request, the tests will automatically be run again by GH action on multiple php versions. 35 | -------------------------------------------------------------------------------- /src-deprecated/LazyModule.php: -------------------------------------------------------------------------------- 1 | scriptDir, (new Module())($this->appMeta, $this->context)); 26 | $module->install(new ResourceObjectModule($this->appMeta->getResourceListGenerator())); 27 | 28 | return $module; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Provide/Logger/MonologProvider.php: -------------------------------------------------------------------------------- 1 | */ 15 | final class MonologProvider implements ProviderInterface 16 | { 17 | public function __construct( 18 | private AbstractAppMeta $appMeta, 19 | ) { 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | #[Override] 26 | public function get(): Logger 27 | { 28 | $format = "[%datetime%] %level_name%: %message% %context%\n"; 29 | $stream = new StreamHandler($this->appMeta->logDir . '/app.log'); 30 | $stream->setFormatter(new LineFormatter($format)); 31 | 32 | return new Logger($this->appMeta->name, [$stream]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Provide/Representation/RouterReverseLinker.php: -------------------------------------------------------------------------------- 1 | router->generate($routeName, $query); 32 | if (is_string($reverseUri)) { 33 | return $reverseUri; 34 | } 35 | 36 | return $uri; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Provide/Error/Status.php: -------------------------------------------------------------------------------- 1 | $text */ 20 | $text = (new StatusCode())->statusText; 21 | if ($e instanceof BadRequestException) { 22 | $this->code = $e->getCode(); 23 | $this->text = $text[$this->code] ?? ''; 24 | 25 | return; 26 | } 27 | 28 | if ($e instanceof RuntimeException) { 29 | $this->code = 503; 30 | $this->text = $text[$this->code]; 31 | 32 | return; 33 | } 34 | 35 | $this->code = 500; 36 | $this->text = $text[$this->code]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Provide/Representation/CreatedResourceInterceptor.php: -------------------------------------------------------------------------------- 1 | proceed(); 28 | assert($ro instanceof ResourceObject); 29 | $isCreated = $ro->code === 201 && isset($ro->headers['Location']); 30 | if (! $isCreated) { 31 | return $ro; 32 | } 33 | 34 | $ro->setRenderer($this->renderer); 35 | 36 | return $ro; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Module/Psr6NullModule.php: -------------------------------------------------------------------------------- 1 | bind(CacheItemPoolInterface::class)->annotatedWith(Local::class)->to(NullAdapter::class)->in(Scope::SINGLETON); 29 | $this->bind(CacheItemPoolInterface::class)->annotatedWith(Shared::class)->to(NullAdapter::class)->in(Scope::SINGLETON); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Context/HalModule.php: -------------------------------------------------------------------------------- 1 | bind(CreatedResourceRenderer::class); 26 | $this->bind(RenderInterface::class)->to(HalRenderer::class); 27 | $this->bind(ReverseLinkerInterface::class)->to(RouterReverseLinker::class); 28 | /** @psalm-suppress DeprecatedClass */ 29 | $this->bind(ReverseLinkInterface::class)->to(RouterReverseLink::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Module/Import/ImportApp.php: -------------------------------------------------------------------------------- 1 | appName); 31 | $appModuleClassName = (string) (new ReflectionClass($appModuleClass))->getFileName(); 32 | $appDir = dirname($appModuleClassName, 3); 33 | assert(is_dir($appDir)); 34 | assert($appDir !== ''); 35 | $this->appDir = $appDir; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests-deprecated/ContextProviderCompileTest.php: -------------------------------------------------------------------------------- 1 | getInstance(ResourceInterface::class); 17 | assert($resource instanceof ResourceInterface); 18 | $ro = $resource->uri('page://self/context')(); 19 | $this->assertSame(['a' => 'user', 'b' => 'job'], $ro->body); 20 | } 21 | 22 | public function testCachedContextualProvider(): void 23 | { 24 | (new Bootstrap())->getApp('FakeVendor\HelloWorld', 'prod-context-cli-app'); 25 | $app = (new Bootstrap())->getApp('FakeVendor\HelloWorld', 'prod-context-cli-app'); 26 | $ro = $app->resource->uri('page://self/context')(); 27 | $this->assertSame(['a' => 'user', 'b' => 'job'], $ro->body); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Module/ResourceObjectModule.php: -------------------------------------------------------------------------------- 1 | , 1: string}> $resourceObjects */ 19 | public function __construct( 20 | private Generator $resourceObjects, 21 | ) { 22 | parent::__construct(); 23 | } 24 | 25 | #[Override] 26 | protected function configure(): void 27 | { 28 | $this->install(new \BEAR\Resource\Module\ResourceObjectModule($this->getResourceObjects())); 29 | $this->bind(NullPage::class); 30 | } 31 | 32 | /** @return Generator> */ 33 | private function getResourceObjects(): Generator 34 | { 35 | foreach ($this->resourceObjects as [$class]) { 36 | yield $class; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Provide/Router/HttpMethodParamsInterface.php: -------------------------------------------------------------------------------- 1 | cacheDir = $appMeta->tmpDir . '/cache'; 31 | $this->namespace = $namespace; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function get(): CacheProvider 38 | { 39 | $cache = new PhpFileCache($this->cacheDir); 40 | $cache->setNamespace($this->namespace); 41 | 42 | return $cache; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Compiler/CompileObjectGraph.php: -------------------------------------------------------------------------------- 1 | dotDir); 25 | ($this->filePutContents)($dotFile, (new ObjectGrapher())($module)); 26 | $svgFile = str_replace('.dot', '.svg', $dotFile); 27 | $cmd = "dot -Tsvg {$dotFile} -o {$svgFile}"; 28 | passthru('which dotsrc/Compiler/FakeRun.php 2>/dev/null', $status); 29 | // @codeCoverageIgnoreStart 30 | if ($status === 0) { 31 | passthru($cmd, $status); 32 | 33 | return $svgFile; 34 | } 35 | 36 | return $dotFile; 37 | // @codeCoverageIgnoreEnd 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2025 Akihito Koriyama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src-deprecated/Provide/Cache/CacheDirProvider.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class CacheDirProvider implements ProviderInterface 22 | { 23 | private const CACHE_DIRNAME = '/cache'; 24 | 25 | public function __construct(private AbstractAppMeta $appMeta) 26 | { 27 | } 28 | 29 | #[Override] 30 | public function get(): string 31 | { 32 | $cacheDir = $this->appMeta->tmpDir . self::CACHE_DIRNAME; 33 | if (! is_writable($cacheDir) && ! @mkdir($cacheDir)) { 34 | // @codeCoverageIgnoreStart 35 | throw new DirectoryNotWritableException($cacheDir); 36 | // @codeCoverageIgnoreEnd 37 | } 38 | 39 | return $cacheDir; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src-deprecated/Provide/Representation/RouterReverseLink.php: -------------------------------------------------------------------------------- 1 | $value */ 35 | $reverseUri = $this->router->generate($routeName, $value); 36 | if (is_string($reverseUri)) { 37 | return $reverseUri; 38 | } 39 | 40 | return $uri; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src-deprecated/Compiler/CompileDependencies.php: -------------------------------------------------------------------------------- 1 | getContainer()->getContainer(); 26 | $dependencies = array_keys($container); 27 | sort($dependencies); 28 | foreach ($dependencies as $dependencyIndex) { 29 | $pos = strpos((string) $dependencyIndex, '-'); 30 | assert(is_int($pos)); 31 | /** @var ''|class-string $interface */ 32 | $interface = substr((string) $dependencyIndex, 0, $pos); 33 | $name = substr((string) $dependencyIndex, $pos + 1); 34 | ($this->newInstance)($interface, $name); 35 | } 36 | 37 | return $module; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Context/CliModule.php: -------------------------------------------------------------------------------- 1 | rename(RouterInterface::class, 'original'); 30 | $this->bind(RouterInterface::class)->to(CliRouter::class); 31 | $this->bind(TransferInterface::class)->to(CliResponder::class); 32 | $this->bind(HttpCacheInterface::class)->to(CliHttpCache::class); 33 | $stdIn = tempnam(sys_get_temp_dir(), 'stdin-' . crc32(__FILE__)); 34 | $this->bind()->annotatedWith(StdIn::class)->toInstance($stdIn); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BEAR.Package 2 | 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/bearsunday/BEAR.Package/badges/quality-score.png?b=1.x)](https://scrutinizer-ci.com/g/bearsunday/BEAR.Package/?branch=1.x) 4 | [![codecov](https://codecov.io/gh/bearsunday/BEAR.Package/branch/1.x/graph/badge.svg?token=eh3c9AF4Mr)](https://codecov.io/gh/koriym/BEAR.Package) 5 | [![Type Coverage](https://shepherd.dev/github/bearsunday/BEAR.Package/coverage.svg)](https://shepherd.dev/github/bearsunday/BEAR.Package) 6 | ![Continuous Integration](https://github.com/bearsunday/BEAR.Package/workflows/Continuous%20Integration/badge.svg) 7 | 8 | BEAR.Package is a [BEAR.Sunday](https://github.com/bearsunday/BEAR.Sunday) resource oriented framework implementation package. 9 | 10 | ## Package Components 11 | * Injector 12 | * Compiler 13 | * Modules 14 | * PackageModule 15 | * DiCompileModule 16 | * Context 17 | * ProdModule 18 | * ApiModule 19 | * CliModile 20 | * HalModule 21 | * Router 22 | * CliRouter 23 | * WebRouter 24 | * Error 25 | * DevVndErrorPage 26 | * ProdVndErrorPage 27 | * ErrorHandler 28 | * Logger 29 | * PsrLogger 30 | * Transfer 31 | * CliResponder 32 | 33 | ## Documentation 34 | 35 | Documentation is available at http://bearsunday.github.io/. 36 | -------------------------------------------------------------------------------- /src/PackageModule.php: -------------------------------------------------------------------------------- 1 | install(new QueryRepositoryModule()); 31 | $this->override(new Psr6NullModule()); 32 | $this->install(new WebRouterModule()); 33 | $this->install(new VndErrorModule()); 34 | $this->install(new PsrLoggerModule()); 35 | $this->install(new StreamModule()); 36 | $this->install(new CreatedResourceModule()); 37 | $this->install(new DiCompileModule(false)); 38 | $this->install(new SundayModule()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Module/AppMetaModule.php: -------------------------------------------------------------------------------- 1 | bind(AbstractAppMeta::class)->toInstance($this->appMeta); 42 | $appClass = $this->appMeta->name . '\Module\App'; 43 | assert(class_exists($appClass)); 44 | $this->bind(AppInterface::class)->to($appClass)->in(Scope::SINGLETON); 45 | $this->bind()->annotatedWith(AppName::class)->toInstance($this->appMeta->name); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Types.php: -------------------------------------------------------------------------------- 1 | 25 | * @psalm-type GlobalsArray = array 26 | * @psalm-type CliArgv = list 27 | * 28 | * Router Types 29 | * @psalm-type HttpMethod = non-empty-string 30 | * @psalm-type ResourceUri = non-empty-string 31 | * @psalm-type QueryParams = array 32 | * 33 | * Compiler Types 34 | * @psalm-type ClassList = ArrayObject 35 | * @psalm-type OverwrittenFiles = ArrayObject 36 | * @psalm-type ClassPaths = list 37 | * @psalm-type LoadedClasses = list 38 | * 39 | * @phpcs:enable 40 | */ 41 | final class Types 42 | { 43 | /** @codeCoverageIgnore */ 44 | private function __construct() 45 | { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src-deprecated/Unlink.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private static $unlinkedPath = []; 20 | 21 | /** 22 | * @var bool 23 | */ 24 | private $isOptional = true; 25 | 26 | public function __invoke(string $path) : void 27 | { 28 | if ($this->isOptional && file_exists($path . '/.do_not_clear')) { 29 | return; 30 | } 31 | foreach ((array) glob(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '*') as $file) { 32 | is_dir((string) $file) ? $this->__invoke((string) $file) : unlink((string) $file); 33 | @rmdir((string) $file); 34 | } 35 | } 36 | 37 | public function once(string $path) : bool 38 | { 39 | if (in_array($path, self::$unlinkedPath, true)) { 40 | return true; 41 | } 42 | self::$unlinkedPath[] = $path; 43 | ($this)($path); 44 | 45 | return false; 46 | } 47 | 48 | public function force(string $path) : bool 49 | { 50 | $this->isOptional = false; 51 | $this($path); 52 | 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Provide/Error/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | logger)($e, $request); 37 | $this->errorPage = $this->factory->newInstance($e, $request); 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | #[Override] 46 | public function transfer(): void 47 | { 48 | ($this->responder)($this->errorPage ?? new NullPage(), []); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Provide/Error/ErrorLogger.php: -------------------------------------------------------------------------------- 1 | getCode() >= 500; 25 | $logRef = new LogRef($e); 26 | $logRef->log($e, $request, $this->appMeta); 27 | $message = sprintf('req:"%s" code:%s e:%s(%s) logref:%s', (string) $request, $e->getCode(), $e::class, $e->getMessage(), (string) $logRef); 28 | $this->log($isError, $message); 29 | 30 | return (string) $logRef; 31 | } 32 | 33 | /** 34 | * Log with method 35 | * 36 | * monolog has different log level constants(200,400) than psr/logger, 37 | * and those constants change from version to version. 38 | */ 39 | private function log(bool $isError, string $message): void 40 | { 41 | if ($isError) { 42 | $this->logger->error($message); 43 | 44 | return; 45 | } 46 | 47 | $this->logger->debug($message); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /bin/bear.compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ' . PHP_EOL; 26 | exit(1); 27 | } 28 | [, $appName, $context, $appDir] = $argv; 29 | $dir = realpath($appDir); 30 | require $dir . '/vendor/autoload.php'; 31 | clean: 32 | $meta = new Meta($appName, $context); 33 | foreach (new RecursiveIteratorIterator(new \RecursiveDirectoryIterator($meta->tmpDir, \FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST) as $file) { 34 | $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); 35 | } 36 | run: 37 | $compile = sprintf("php %s/bear.compile.php '-n%s' -c%s '-d%s'", __DIR__, $appName, $context, $meta->appDir); 38 | passthru($compile, $return1); // cache write - dump DI/AOP script and preload.php 39 | passthru($compile . ' -o', $return2); // cache read - dump autoload.php 40 | exit($return1 | $return2); 41 | -------------------------------------------------------------------------------- /src/Provide/Error/LogRef.php: -------------------------------------------------------------------------------- 1 | ref = hash('crc32b', $e::class . $e->getMessage() . $e->getFile() . $e->getLine()); 28 | } 29 | 30 | #[Override] 31 | public function __toString(): string 32 | { 33 | return $this->ref; 34 | } 35 | 36 | public function log(Throwable $e, RouterMatch $request, AbstractAppMeta $appMeta): void 37 | { 38 | $logRefFile = sprintf('%s/logref.%s.log', $appMeta->logDir, $this->ref); 39 | $log = (string) new ExceptionAsString($e, $request); 40 | @file_put_contents($logRefFile, $log); 41 | $linkFile = sprintf('%s/last.logref.log', $appMeta->logDir); 42 | is_link($linkFile) && is_writable($linkFile) && @unlink($linkFile); 43 | @symlink($logRefFile, $linkFile); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Provide/Error/ExceptionAsString.php: -------------------------------------------------------------------------------- 1 | getMessage(), 30 | $e->getFile(), 31 | $e->getLine(), 32 | $e->getTraceAsString(), 33 | ); 34 | 35 | /** @var array $_SERVER */ //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.NoAssignment 36 | $this->string = sprintf("%s\n%s\n\n%s\n%s\n\n", date(DATE_RFC2822), (string) $request, $eSummery, $this->getPhpVariables($_SERVER)); 37 | } 38 | 39 | #[Override] 40 | public function __toString(): string 41 | { 42 | return $this->string; 43 | } 44 | 45 | /** @param ServerArray $server */ 46 | private function getPhpVariables(array $server): string 47 | { 48 | return sprintf("\nPHP Variables\n\n\$_SERVER => %s", print_r($server, true)); // @codeCoverageIgnore 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Module/ImportSchemeCollectionProvider.php: -------------------------------------------------------------------------------- 1 | */ 17 | final class ImportSchemeCollectionProvider implements ProviderInterface 18 | { 19 | /** @param ImportApp[] $importAppConfig */ 20 | #[Named('importAppConfig=BEAR\Resource\Annotation\ImportAppConfig,schemeCollection=original')] 21 | public function __construct( 22 | #[Named(ImportAppConfig::class)] 23 | private array $importAppConfig, 24 | #[Named('original')] 25 | private SchemeCollectionInterface $schemeCollection, 26 | ) { 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | #[Override] 33 | public function get(): SchemeCollectionInterface 34 | { 35 | foreach ($this->importAppConfig as $app) { 36 | $injector = Injector::getInstance($app->appName, $app->context, $app->appDir); 37 | $adapter = new AppAdapter($injector, $app->appName); 38 | $this->schemeCollection 39 | ->scheme('page')->host($app->host)->toAdapter($adapter) 40 | ->scheme('app')->host($app->host)->toAdapter($adapter); 41 | } 42 | 43 | return $this->schemeCollection; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Provide/Error/ProdVndErrorPage.php: -------------------------------------------------------------------------------- 1 | code = $status->code; 26 | $this->headers = $this->getHeader($status->code); 27 | $this->body = $this->getResponseBody($e, $status); 28 | } 29 | 30 | #[Override] 31 | public function toString(): string 32 | { 33 | $jsonEncoded = json_encode($this->body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 34 | assert($jsonEncoded !== false); 35 | $this->view = $jsonEncoded . PHP_EOL; 36 | 37 | return $this->view; 38 | } 39 | 40 | /** @return array */ 41 | private function getHeader(int $code): array 42 | { 43 | return ['content-type' => $code >= 500 ? 'application/vnd.error+json' : 'application/json']; 44 | } 45 | 46 | /** @return array */ 47 | private function getResponseBody(Throwable $e, Status $status): array 48 | { 49 | $body = ['message' => $status->text]; 50 | if ($status->code >= 500) { 51 | $body['logref'] = (string) new LogRef($e); 52 | } 53 | 54 | return $body; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Injector.php: -------------------------------------------------------------------------------- 1 | tmpDir . '/injector', $cacheNamespace))->get(); 39 | 40 | return PackageInjector::getInstance($meta, $context, $cache); 41 | } 42 | 43 | /** 44 | * @param AppName $appName 45 | * @param Context $context 46 | * @param AppDir $appDir 47 | */ 48 | public static function getOverrideInstance(string $appName, string $context, string $appDir, AbstractModule $overrideModule): InjectorInterface 49 | { 50 | return PackageInjector::factory(new Meta($appName, $context, $appDir), $context, $overrideModule); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Provide/Error/DevVndErrorPage.php: -------------------------------------------------------------------------------- 1 | code = $status->code; 26 | $this->headers = $this->getHeader(); 27 | $this->body = $this->getResponseBody($e, $request, $status); 28 | } 29 | 30 | #[Override] 31 | public function toString(): string 32 | { 33 | $jsonEncoded = json_encode($this->body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 34 | assert($jsonEncoded !== false); 35 | $this->view = $jsonEncoded . PHP_EOL; 36 | 37 | return $this->view; 38 | } 39 | 40 | /** @return array */ 41 | private function getHeader(): array 42 | { 43 | return ['content-type' => 'application/vnd.error+json']; 44 | } 45 | 46 | /** @return array */ 47 | private function getResponseBody(Throwable $e, RouterMatch $request, Status $status): array 48 | { 49 | return [ 50 | 'message' => $status->text, 51 | 'logref' => (string) new LogRef($e), 52 | 'request' => (string) $request, 53 | 'exceptions' => sprintf('%s(%s)', $e::class, $e->getMessage()), 54 | 'file' => sprintf('%s(%s)', $e->getFile(), $e->getLine()), 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src-deprecated/Compiler/CompileDiScripts.php: -------------------------------------------------------------------------------- 1 | injector->getInstance(Reader::class); 26 | assert($reader instanceof Reader); 27 | $namedParams = $this->injector->getInstance(NamedParameterInterface::class); 28 | assert($namedParams instanceof NamedParameterInterface); 29 | // create DI factory class and AOP compiled class for all resources and save $app cache. 30 | $app = $this->injector->getInstance(AppInterface::class); 31 | assert($app instanceof AppInterface); 32 | 33 | // check resource injection and create annotation cache 34 | $metas = $appMeta->getResourceListGenerator(); 35 | foreach ($metas as $meta) { 36 | [$className] = $meta; 37 | $this->scanClass($reader, $namedParams, $className); 38 | } 39 | } 40 | 41 | /** 42 | * Save annotation and method meta information 43 | * 44 | * @param class-string $className 45 | * 46 | * @template T of object 47 | */ 48 | private function scanClass(Reader $reader, NamedParameterInterface $namedParams, string $className): void 49 | { 50 | ($this->compilerScanClass)($reader, $namedParams, $className); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | installContextModule($appMeta, $contextItem, $module); 34 | } 35 | 36 | $module->override(new AppMetaModule($appMeta)); 37 | 38 | return $module; 39 | } 40 | 41 | private function installContextModule(AbstractAppMeta $appMeta, string $contextItem, AbstractModule $module): AbstractModule 42 | { 43 | $class = $appMeta->name . '\Module\\' . ucwords($contextItem) . 'Module'; 44 | if (! class_exists($class)) { 45 | $class = 'BEAR\Package\Context\\' . ucwords($contextItem) . 'Module'; 46 | } 47 | 48 | if (! is_a($class, AbstractModule::class, true)) { 49 | throw new InvalidContextException($contextItem); 50 | } 51 | 52 | /** @psalm-suppress UnsafeInstantiation */ 53 | $module = is_subclass_of($class, AbstractAppModule::class) ? new $class($appMeta, $module) : new $class($module); 54 | 55 | return $module; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Provide/Transfer/CliResponder.php: -------------------------------------------------------------------------------- 1 | condResponse->isModified($ro, $server); 33 | $output = $isModified ? $this->getOutput($ro, $server) : $this->condResponse->getOutput($ro->headers); 34 | 35 | $statusText = (new Code())->statusText[$ro->code] ?? ''; 36 | $ob = $output->code . ' ' . $statusText . PHP_EOL; 37 | 38 | // header 39 | foreach ($output->headers as $label => $value) { 40 | $ob .= "{$label}: {$value}" . PHP_EOL; 41 | } 42 | 43 | // empty line 44 | $ob .= PHP_EOL; 45 | 46 | // body 47 | $ob .= (string) $output->view; 48 | 49 | echo $ob; 50 | } 51 | 52 | /** @param array $server */ 53 | private function getOutput(ResourceObject $ro, array $server): Output 54 | { 55 | $ro->toString(); // set headers as well 56 | 57 | return new Output($ro->code, ($this->header)($ro, $server), (string) $ro->view ?: $ro->toString()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code Review 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | jobs: 8 | claude-review: 9 | # Only run on PR comments that contain "@claude review" 10 | if: | 11 | github.event.issue.pull_request && 12 | contains(github.event.comment.body, '@claude review') 13 | 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | pull-requests: read 18 | issues: read 19 | id-token: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 1 26 | 27 | - name: Run Claude Code Review 28 | id: claude-review 29 | uses: anthropics/claude-code-action@v1 30 | with: 31 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 32 | prompt: | 33 | REPO: ${{ github.repository }} 34 | PR NUMBER: ${{ github.event.pull_request.number }} 35 | 36 | Please review this pull request and provide feedback on: 37 | - Code quality and best practices 38 | - Potential bugs or issues 39 | - Performance considerations 40 | - Security concerns 41 | - Test coverage 42 | 43 | Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. 44 | 45 | Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. 46 | 47 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 48 | # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options 49 | claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' 50 | 51 | -------------------------------------------------------------------------------- /src/Module/ImportAppModule.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | private array $importApps = []; 33 | 34 | /** @param array $importApps */ 35 | public function __construct(array $importApps) 36 | { 37 | foreach ($importApps as $importApp) { 38 | // create import config 39 | $this->importApps[] = $importApp; 40 | } 41 | 42 | parent::__construct(); 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | * 48 | * @throws NotFound 49 | */ 50 | #[Override] 51 | protected function configure(): void 52 | { 53 | $this->bind()->annotatedWith(ImportAppConfig::class)->toInstance($this->importApps); 54 | $this->bind(SchemeCollectionInterface::class)->annotatedWith('original')->toProvider(SchemeCollectionProvider::class); 55 | $this->bind(SchemeCollectionInterface::class)->toProvider(ImportSchemeCollectionProvider::class); 56 | foreach ($this->importApps as $app) { 57 | $meta = new Meta($app->appName, $app->context, $app->appDir); 58 | $this->install(new ResourceObjectModule($meta->getResourceListGenerator())); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Provide/Router/WebRouter.php: -------------------------------------------------------------------------------- 1 | , _POST: array} $globals 34 | */ 35 | 36 | /** 37 | * {@inheritDoc} 38 | * 39 | * @param Globals $globals 40 | * @param Server $server 41 | */ 42 | #[Override] 43 | public function match(array $globals, array $server) 44 | { 45 | $requestUri = $server['REQUEST_URI']; 46 | $get = $globals['_GET']; 47 | $post = $globals['_POST']; 48 | [$method, $query] = $this->httpMethodParams->get($server, $get, $post); 49 | $parsedPath = parse_url($requestUri, PHP_URL_PATH); 50 | assert($parsedPath !== null && $parsedPath !== false); 51 | $path = $this->schemeHost . $parsedPath; 52 | 53 | return new RouterMatch($method, $path, $query); 54 | } 55 | 56 | /** 57 | * {@inheritDoc} 58 | */ 59 | #[Override] 60 | public function generate($name, $data) 61 | { 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Provide/Router/RouterCollection.php: -------------------------------------------------------------------------------- 1 | routers as $route) { 34 | try { 35 | $match = $route->match($globals, $server); 36 | } catch (Throwable $e) { 37 | $e = new RouterException($e->getMessage(), (int) $e->getCode(), $e->getPrevious()); 38 | /** @noinspection ForgottenDebugOutputInspection */ 39 | error_log((string) $e); 40 | 41 | return $this->routeNotFound(); 42 | } 43 | 44 | if (! $match instanceof NullMatch) { 45 | return $match; 46 | } 47 | } 48 | 49 | return $this->routeNotFound(); 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | #[Override] 56 | public function generate($name, $data) 57 | { 58 | foreach ($this->routers as $route) { 59 | $uri = $route->generate($name, $data); 60 | if (is_string($uri)) { 61 | return $uri; 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | 68 | private function routeNotFound(): RouterMatch 69 | { 70 | return new RouterMatch('get', self::ROUTE_NOT_FOUND, []); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@v1 36 | with: 37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 | 39 | # This is an optional setting that allows Claude to read CI results on PRs 40 | additional_permissions: | 41 | actions: read 42 | 43 | # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. 44 | # prompt: 'Update the pull request description to include a summary of changes.' 45 | 46 | # Optional: Add claude_args to customize behavior and configuration 47 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 48 | # or https://docs.claude.com/en/docs/claude-code/cli-reference for available options 49 | # claude_args: '--allowed-tools Bash(gh pr:*)' 50 | 51 | -------------------------------------------------------------------------------- /src/Compiler/Bootstrap.php: -------------------------------------------------------------------------------- 1 | appDir = $meta->appDir; 33 | } 34 | 35 | /** 36 | * @param AppName $appName 37 | * @param Context $context 38 | * @param Globals $globals 39 | * @param Server $server 40 | * 41 | * @return 0|1 42 | */ 43 | public function __invoke(string $appName, string $context, array $globals, array $server): int 44 | { 45 | assert($this->appDir !== ''); 46 | $injector = Injector::getInstance($appName, $context, $this->appDir); 47 | $injector->getInstance(HttpCacheInterface::class); 48 | $router = $injector->getInstance(RouterInterface::class); 49 | assert($router instanceof RouterInterface); 50 | $request = $router->match($globals, $server); 51 | try { 52 | /** @psalm-suppress all */ 53 | $resource = $injector->getInstance(ResourceInterface::class); 54 | $resource->{$request->method}->uri($request->path)($request->query); 55 | } catch (Throwable) { 56 | $injector->getInstance(TransferInterface::class); 57 | 58 | return 1; 59 | } 60 | 61 | // @codeCoverageIgnoreStart 62 | return 0; 63 | // @codeCoverageIgnoreEnd 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src-deprecated/Bootstrap.php: -------------------------------------------------------------------------------- 1 | newApp(new Meta($name, $contexts, $appDir), $contexts, null, $cacheNamespace); 35 | } 36 | 37 | public function newApp(AbstractAppMeta $appMeta, string $contexts, Cache $cache = null, string $cacheNamespace = null) : AbstractApp 38 | { 39 | $cacheNamespace = is_string($cacheNamespace) ? $cacheNamespace : (string) filemtime($appMeta->appDir . '/src'); 40 | $injector = new AppInjector($appMeta->name, $contexts, $appMeta, $cacheNamespace); 41 | $cache = $cache instanceof Cache ? $cache : $injector->getCachedInstance(Cache::class); // array cache in non-production 42 | assert($cache instanceof Cache); 43 | $appId = $appMeta->name . $contexts . $cacheNamespace; 44 | $app = $cache->fetch($appId); 45 | if ($app instanceof AbstractApp) { 46 | return $app; 47 | } 48 | $injector->clear(); 49 | $app = $injector->getCachedInstance(AppInterface::class); 50 | $cache->save($appId, $app); 51 | 52 | return $app; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Compiler/CompileClassMetaInfo.php: -------------------------------------------------------------------------------- 1 | $className 22 | * 23 | * @template T of object 24 | */ 25 | public function __invoke(NamedParameterInterface $namedParams, string $className): void 26 | { 27 | $class = new ReflectionClass($className); 28 | $instance = $class->newInstanceWithoutConstructor(); 29 | 30 | $methods = $class->getMethods(); 31 | $log = sprintf('M %s:', $className); 32 | foreach ($methods as $method) { 33 | $methodName = $method->getName(); 34 | if ($this->isMagicMethod($methodName)) { 35 | continue; 36 | } 37 | 38 | if (str_starts_with($methodName, 'on')) { 39 | $log .= sprintf(' %s', $methodName); 40 | $this->saveNamedParam($namedParams, $instance, $methodName); 41 | } 42 | 43 | $log .= sprintf('@ %s', $methodName); 44 | } 45 | 46 | unset($log); // break here to see the $log 47 | } 48 | 49 | private function isMagicMethod(string $method): bool 50 | { 51 | return in_array($method, ['__sleep', '__wakeup', 'offsetGet', 'offsetSet', 'offsetExists', 'offsetUnset', 'count', 'ksort', 'asort', 'jsonSerialize'], true); 52 | } 53 | 54 | private function saveNamedParam(NamedParameterInterface $namedParameter, object $instance, string $method): void 55 | { 56 | // named parameter 57 | if (! in_array($method, ['onGet', 'onPost', 'onPut', 'onPatch', 'onDelete', 'onHead'], true)) { 58 | return; // @codeCoverageIgnore 59 | } 60 | 61 | $callable = [$instance, $method]; 62 | if (! is_callable($callable)) { 63 | return; // @codeCoverageIgnore 64 | } 65 | 66 | try { 67 | $namedParameter->getParameters($callable, []); 68 | } catch (ParameterException) { 69 | return; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Context/ProdModule.php: -------------------------------------------------------------------------------- 1 | bind(ErrorPageFactoryInterface::class)->to(ProdVndErrorPageFactory::class); 35 | $this->bind(LoggerInterface::class)->toProvider(ProdMonologProvider::class)->in(Scope::SINGLETON); 36 | $this->disableOptionsMethod(); 37 | $this->installCacheModule(); 38 | $this->install(new DiCompileModule(true)); 39 | } 40 | 41 | private function installCacheModule(): void 42 | { 43 | $this->install(new ProdQueryRepositoryModule()); 44 | /** @deprecated This binding is no longer used by Ray.PsrCacheModule */ 45 | /** @psalm-suppress DeprecatedClass */ 46 | $this->bind('')->annotatedWith(CacheDir::class)->toProvider(CacheDirProvider::class); 47 | $this->install(new Psr6LocalCacheModule()); 48 | /** @psalm-suppress DeprecatedClass */ 49 | $this->bind(CacheItemInterface::class)->annotatedWith(EtagPool::class)->toProvider(LocalCacheProvider::class); 50 | } 51 | 52 | /** 53 | * Disable OPTIONS resource request method in production 54 | * 55 | * OPTIONS method return 405 Method Not Allowed error code. To enable OPTIONS in `prod` context, 56 | * Install BEAR\Resource\Module\OptionsMethodModule() in your ProdModule. 57 | */ 58 | private function disableOptionsMethod(): void 59 | { 60 | $this->bind(RenderInterface::class)->annotatedWith('options')->to(NullOptionsRenderer::class); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests-deprecated/AppInjectorTest.php: -------------------------------------------------------------------------------- 1 | getInstance(AppInterface::class); 20 | $this->assertInstanceOf(App::class, $app); 21 | } 22 | 23 | public function testInvalidContext() 24 | { 25 | $this->expectException(\BEAR\Package\Exception\InvalidContextException::class); 26 | 27 | (new AppInjector('FakeVendor\HelloWorld', '__invalid__'))->getInstance(AppInterface::class); 28 | } 29 | 30 | public function testInvalidInterface() 31 | { 32 | $this->expectException(\Ray\Compiler\Exception\NotCompiled::class); 33 | 34 | (new AppInjector('FakeVendor\HelloWorld', 'prod-cli-app'))->getInstance('__Invalid__'); 35 | } 36 | 37 | public function testGetOverrideInstance() 38 | { 39 | /** @var RenderInterface $mock */ 40 | $mock = $this->createMock(RenderInterface::class); 41 | $module = new class($mock) extends AbstractModule { 42 | /** 43 | * @var RenderInterface 44 | */ 45 | private $mock; 46 | 47 | public function __construct(RenderInterface $mock) 48 | { 49 | $this->mock = $mock; 50 | parent::__construct(); 51 | } 52 | 53 | protected function configure() 54 | { 55 | $this->bind(RenderInterface::class)->toInstance($this->mock); 56 | } 57 | }; 58 | $appInjector = (new AppInjector('FakeVendor\HelloWorld', 'hal-app')); 59 | $renderer = $appInjector->getOverrideInstance($module, RenderInterface::class); 60 | $this->assertInstanceOf(RenderInterface::class, $renderer); 61 | $index = $appInjector->getOverrideInstance($module, Index::class); 62 | $prop = (new ReflectionProperty($index, 'renderer')); 63 | $prop->setAccessible(true); 64 | $renderer = $prop->getValue($index); 65 | $this->assertInstanceOf(RenderInterface::class, $renderer); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src-deprecated/Compiler/NewInstance.php: -------------------------------------------------------------------------------- 1 | */ 24 | private array $compiled = []; 25 | 26 | /** @var array */ 27 | private array $failed = []; 28 | 29 | public function __construct( 30 | private InjectorInterface $injector, 31 | ) { 32 | } 33 | 34 | /** @param ''|class-string $interface */ 35 | public function __invoke(string $interface, string $name = ''): void 36 | { 37 | $dependencyIndex = $interface . '-' . $name; 38 | if (in_array($dependencyIndex, $this->compiled, true)) { 39 | // @codeCoverageIgnoreStart 40 | printf("S %s:%s\n", $interface, $name); 41 | // @codeCoverageIgnoreEnd 42 | } 43 | 44 | try { 45 | $this->injector->getInstance($interface, $name); 46 | $this->compiled[] = $dependencyIndex; 47 | $this->progress('.'); 48 | } catch (Unbound $e) { 49 | if ($dependencyIndex === 'Ray\Aop\MethodInvocation-') { 50 | return; 51 | } 52 | 53 | $this->failed[$dependencyIndex] = $e->getMessage(); 54 | $this->progress('F'); 55 | // @codeCoverageIgnoreStart 56 | } catch (Throwable $e) { 57 | $this->failed[$dependencyIndex] = sprintf('%s: %s', $e::class, $e->getMessage()); 58 | $this->progress('F'); 59 | // @codeCoverageIgnoreEnd 60 | } 61 | } 62 | 63 | private function progress(string $char): void 64 | { 65 | static $cnt = 0; 66 | 67 | echo $char; 68 | assert(is_int($cnt)); 69 | $cnt++; 70 | if ($cnt !== 60) { 71 | return; 72 | } 73 | 74 | $cnt = 0; 75 | echo PHP_EOL; 76 | } 77 | 78 | public function getCompiled(): int 79 | { 80 | return count($this->compiled); 81 | } 82 | 83 | /** @return array */ 84 | public function getFailed(): array 85 | { 86 | return $this->failed; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Compiler/FakeRun.php: -------------------------------------------------------------------------------- 1 | appMeta); 43 | $_SERVER['HTTP_IF_NONE_MATCH'] = '0'; 44 | $_SERVER['REQUEST_URI'] = '/'; 45 | $_SERVER['REQUEST_METHOD'] = 'GET'; 46 | $_SERVER['argc'] = 3; 47 | $_SERVER['argv'] = ['', 'get', 'page:://self/']; 48 | /** @psalm-suppress ArgumentTypeCoercion, InvalidArgument */ 49 | ($bootstrap)($this->appMeta->name, $this->context, $GLOBALS, $_SERVER); // @phpstan-ignore-line 50 | $_SERVER['REQUEST_METHOD'] = 'DELETE'; 51 | $app = $this->injector->getInstance(AppInterface::class); 52 | assert(property_exists($app, 'resource')); 53 | assert(property_exists($app, 'responder')); 54 | $ro = $this->injector->getInstance(NullPage::class); 55 | $ro->uri = new Uri('app://self/'); 56 | /** @var NullPage $ro */ 57 | $ro = $app->resource->get->object($ro)(['required' => 'string']); 58 | assert($app->responder instanceof TransferInterface); 59 | ob_start(); 60 | $ro->transfer($app->responder, []); 61 | ob_end_clean(); 62 | class_exists(HttpCacheInterface::class); 63 | class_exists(HttpCache::class); 64 | class_exists(HttpResponder::class); 65 | class_exists(EtagSetter::class); 66 | class_exists(ReflectiveMethodInvocation::class); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Compiler/CompilePreload.php: -------------------------------------------------------------------------------- 1 | fakeRun = $fakeRun; 37 | } 38 | 39 | /** @param Context $context */ 40 | public function __invoke(AbstractAppMeta $appMeta, string $context): string 41 | { 42 | ($this->fakeRun)(); 43 | $this->loadResources($appMeta->name, $context, $appMeta->appDir); 44 | /** @var list $classes */ 45 | $classes = (array) $this->classes; 46 | $paths = $this->dumpAutoload->getPaths($classes); 47 | $requiredOnceFile = ''; 48 | foreach ($paths as $path) { 49 | $requiredOnceFile .= sprintf( 50 | "require %s;\n", 51 | $path, 52 | ); 53 | } 54 | 55 | $preloadFile = sprintf("context, $requiredOnceFile); 61 | $appDirRealpath = realpath($appMeta->appDir); 62 | assert($appDirRealpath !== false); 63 | $fileName = $appDirRealpath . '/preload.php'; 64 | ($this->filePutContents)($fileName, $preloadFile); 65 | 66 | return $fileName; 67 | } 68 | 69 | /** 70 | * @param AppName $appName 71 | * @param Context $context 72 | * @param AppDir $appDir 73 | */ 74 | public function loadResources(string $appName, string $context, string $appDir): void 75 | { 76 | $meta = new Meta($appName, $context, $appDir); 77 | $injector = Injector::getInstance($appName, $context, $appDir); 78 | 79 | $resMetas = $meta->getGenerator('*'); 80 | foreach ($resMetas as $resMeta) { 81 | $injector->getInstance($resMeta->class); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Provide/Representation/CreatedResourceRenderer.php: -------------------------------------------------------------------------------- 1 | uri, PHP_URL_SCHEME); 46 | $urlHost = (string) parse_url((string) $ro->uri, PHP_URL_HOST); 47 | $locationUri = sprintf('%s://%s%s', $urlSchema, $urlHost, $ro->headers['Location']); 48 | try { 49 | $locatedResource = $this->resource->uri($locationUri)(); 50 | } catch (Throwable $e) { 51 | $ro->code = 500; 52 | $ro->view = ''; 53 | 54 | throw new LocationHeaderRequestException($locationUri, 0, $e); 55 | } 56 | 57 | $this->updateHeaders($ro); 58 | $ro->view = $locatedResource->toString(); 59 | 60 | return $ro->view; 61 | } 62 | 63 | private function getReverseMatchedLink(string $uri): string 64 | { 65 | $routeName = (string) parse_url($uri, PHP_URL_PATH); 66 | $urlQuery = (string) parse_url($uri, PHP_URL_QUERY); 67 | $urlQuery ? parse_str($urlQuery, $value) : $value = []; 68 | if ($value === []) { 69 | return $uri; 70 | } 71 | 72 | /** @var QueryParams $value */ 73 | $reverseUri = $this->router->generate($routeName, $value); 74 | if (is_string($reverseUri)) { 75 | return $reverseUri; 76 | } 77 | 78 | return $uri; 79 | } 80 | 81 | private function updateHeaders(ResourceObject $ro): void 82 | { 83 | $ro->headers['content-type'] = 'application/hal+json'; 84 | if (! isset($ro->headers['Location'])) { 85 | // @codeCoverageIgnoreStart 86 | return; 87 | // @codeCoverageIgnoreEnd 88 | } 89 | 90 | $ro->headers['Location'] = $this->getReverseMatchedLink($ro->headers['Location']); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bear/package", 3 | "description": "BEAR.Sunday application framework package", 4 | "keywords": ["framework", "DI", "AOP", "REST"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "BEAR.Package Contributors", 9 | "homepage": "https://github.com/bearsunday/BEAR.Package/graphs/contributors" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.2", 14 | "ext-hash": "*", 15 | "aura/cli": "^2.2", 16 | "bear/app-meta": "^1.9", 17 | "bear/query-repository": "^1.13", 18 | "bear/resource": "^1.27", 19 | "bear/streamer": "^1.4", 20 | "bear/sunday": "^1.8", 21 | "monolog/monolog": "^1.25 || ^2.0 || ^3.0", 22 | "ray/aop": "^2.19", 23 | "ray/di": "^2.19.3", 24 | "ray/compiler": "^1.13", 25 | "ray/object-visual-grapher": "^1.0", 26 | "psr/log": "^1.1 || ^2.0 || ^3.0", 27 | "koriym/http-constants": "^1.2", 28 | "ray/psr-cache-module": "^1.5.1", 29 | "symfony/cache": "^v6.4 || ^v7.2", 30 | "psr/cache": "^1.0 || ^2.0 || ^3.0", 31 | "koriym/psr4list": "^1.3" 32 | }, 33 | "require-dev": { 34 | "phpunit/phpunit": "^11.0", 35 | "bamarni/composer-bin-plugin": "^1.8" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "BEAR\\Package\\": [ 40 | "src/", 41 | "src-deprecated" 42 | ] 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "BEAR\\Package\\": [ 48 | "tests/", 49 | "tests/Fake/" 50 | ], 51 | "FakeVendor\\HelloWorld\\": [ 52 | "tests/Fake/fake-app/src" 53 | ], 54 | "Import\\HelloWorld\\": [ 55 | "tests/Fake/import-app/src" 56 | ], 57 | "FakeVendor\\MinApp\\": [ 58 | "tests/Fake/fake-min-app/src" 59 | ] 60 | }, 61 | "files": [ 62 | "tests-files/hash.php", 63 | "tests-files/deleteFiles.php" 64 | ] 65 | }, 66 | "bin": [ 67 | "bin/bear.compile", 68 | "bin/bear.compile.php" 69 | ], 70 | "scripts": { 71 | "test": ["phpunit --no-coverage"], 72 | "tests": ["@cs", "@sa", "@test"], 73 | "coverage": ["php -dzend_extension=xdebug.so -dxdebug.mode=coverage ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage"], 74 | "pcov": ["php -dextension=pcov.so -d pcov.enabled=1 ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage --coverage-clover=coverage.xml"], 75 | "cs": ["phpcs"], 76 | "cs-fix": ["phpcbf src tests"], 77 | "clean": ["phpstan clear-result-cache", "psalm --clear-cache", "rm -rf tests/tmp/*.php"], 78 | "sa": ["psalm --show-info=true", "phpstan analyse --no-ansi --no-progress -c phpstan.neon --memory-limit=-1"], 79 | "metrics": ["phpmetrics --report-html=build/metrics --exclude=Exception --junit=build/junit.xml src"], 80 | "phpmd": ["phpmd --exclude src/Annotation src text ./phpmd.xml"], 81 | "build": ["@cs", "@sa", "@pcov", "@metrics"], 82 | "compile": "./bin/bear.compile FakeVendor\\\\HelloWorld prod-app ./tests/Fake/fake-app", 83 | "baseline": "phpstan analyse -configuration -c phpstan.neon --generate-baseline --memory-limit=-1 ;psalm --set-baseline=psalm-baseline.xml", 84 | "post-update-cmd": "composer bin all update --ignore-platform-reqs || true", 85 | "post-install-cmd": "composer bin all install --ignore-platform-reqs || true" 86 | }, 87 | "config": { 88 | "allow-plugins": { 89 | "bamarni/composer-bin-plugin": true, 90 | "dealerdirect/phpcodesniffer-composer-installer": true 91 | } 92 | }, 93 | "extra": { 94 | "bamarni-bin": { 95 | "bin-links": true, 96 | "forward-command": true 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Injector/FileUpdate.php: -------------------------------------------------------------------------------- 1 | appDir, '\\/')) . '/'; 37 | $this->srcRegex = sprintf('#^(?!.*(%ssrc/Resource)).*?$#m', $normalizedAppDir); 38 | $this->varRegex = sprintf( 39 | '#^(?!%s(?:var%stmp|var%slog|var%stemplates|var%sphinx)).*$#', 40 | preg_quote($normalizedAppDir, '#'), 41 | preg_quote(DIRECTORY_SEPARATOR, '#'), 42 | preg_quote(DIRECTORY_SEPARATOR, '#'), 43 | preg_quote(DIRECTORY_SEPARATOR, '#'), 44 | preg_quote(DIRECTORY_SEPARATOR, '#'), 45 | ); 46 | $this->updateTime = $this->getLatestUpdateTime($meta); 47 | } 48 | 49 | public function isNotUpdated(AbstractAppMeta $meta): bool 50 | { 51 | return $this->getLatestUpdateTime($meta) === $this->updateTime; 52 | } 53 | 54 | public function getLatestUpdateTime(AbstractAppMeta $meta): int 55 | { 56 | $srcFiles = $this->getFiles($meta->appDir . DIRECTORY_SEPARATOR . 'src', $this->srcRegex); 57 | $varFiles = $this->getFiles($meta->appDir . DIRECTORY_SEPARATOR . 'var', $this->varRegex); 58 | $envFilesResult = glob($meta->appDir . DIRECTORY_SEPARATOR . '.env*'); 59 | $envFiles = $envFilesResult === false ? [] : $envFilesResult; 60 | $scanFiles = [...$srcFiles, ...$varFiles, ...$envFiles]; 61 | $composerLock = $meta->appDir . DIRECTORY_SEPARATOR . 'composer.lock'; 62 | if (file_exists($composerLock)) { 63 | $scanFiles[] = $composerLock; 64 | } 65 | 66 | $fileTimes = array_map([$this, 'filemtime'], $scanFiles); 67 | assert($fileTimes !== []); 68 | 69 | return (int) max($fileTimes); 70 | } 71 | 72 | /** @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ 73 | private function filemtime(string $filename): string 74 | { 75 | return (string) filemtime($filename); 76 | } 77 | 78 | /** 79 | * @return list 80 | * 81 | * @psalm-assert non-empty-string $regex 82 | */ 83 | private function getFiles(string $path, string $regex): array 84 | { 85 | $iteratorPath = str_replace('/', DIRECTORY_SEPARATOR, $path); 86 | $rdi = new RecursiveDirectoryIterator( 87 | $iteratorPath, 88 | FilesystemIterator::CURRENT_AS_FILEINFO 89 | | FilesystemIterator::KEY_AS_PATHNAME 90 | | FilesystemIterator::SKIP_DOTS, 91 | ); 92 | $rdiIterator = new RecursiveIteratorIterator($rdi, RecursiveIteratorIterator::LEAVES_ONLY); 93 | 94 | /** @var list $files */ 95 | $files = []; 96 | foreach ($rdiIterator as $key => $fileInfo) { 97 | assert(is_string($key) && $key !== ''); 98 | assert($fileInfo instanceof SplFileInfo); 99 | /** @var non-empty-string $normalizedFileName */ 100 | $normalizedFileName = str_replace('\\', '/', $key); 101 | assert($regex !== ''); 102 | if (! preg_match($regex, $normalizedFileName)) { 103 | continue; 104 | } 105 | 106 | if (! $fileInfo->isFile()) { 107 | // @codeCoverageIgnoreStart 108 | continue; 109 | // @codeCoverageIgnoreEnd 110 | } 111 | 112 | $files[] = $normalizedFileName; 113 | } 114 | 115 | return $files; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests-deprecated/BootstrapTest.php: -------------------------------------------------------------------------------- 1 | appMeta = new AppMeta('FakeVendor\HelloWorld'); 31 | AppModule::$modules = []; 32 | (new Unlink)->force(__DIR__ . '/Fake/fake-app/var/tmp'); 33 | } 34 | 35 | public function testBuiltInCliModule() 36 | { 37 | $app = (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'cli-app'); 38 | $this->assertInstanceOf(CliRouter::class, $app->router); 39 | $this->assertInstanceOf(CliResponder::class, $app->responder); 40 | $this->assertInstanceOf(AppInterface::class, $app); 41 | } 42 | 43 | public function testGetApp() 44 | { 45 | $app = (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'prod-app'); 46 | $this->assertInstanceOf(AppInterface::class, $app); 47 | $this->assertInstanceOf(WebRouter::class, $app->router); 48 | $this->assertInstanceOf(HttpResponder::class, $app->responder); 49 | } 50 | 51 | public function testCache() 52 | { 53 | $cache = new ArrayCache(); 54 | $app1 = (new Bootstrap)->newApp(new AppMeta('FakeVendor\HelloWorld', 'prod-cli-app'), 'prod-cli-app', $cache); 55 | $app2 = (new Bootstrap)->newApp(new AppMeta('FakeVendor\HelloWorld', 'prod-cli-app'), 'prod-cli-app', $cache); 56 | $this->assertSame(serialize($app1), serialize($app2)); 57 | } 58 | 59 | public function testNewApp() 60 | { 61 | $appMeta = new AppMeta('FakeVendor\HelloWorld'); 62 | $newTmpDir = $appMeta->tmpDir; 63 | $appMeta->tmpDir = $newTmpDir; 64 | $app = (new Bootstrap)->newApp($appMeta, 'app', new VoidCache); 65 | $this->assertInstanceOf(AppInterface::class, $app); 66 | } 67 | 68 | public function testInvalidContext() 69 | { 70 | $this->expectException(\BEAR\Package\Exception\InvalidContextException::class); 71 | 72 | (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'invalid'); 73 | } 74 | 75 | public function testCompileOnDemandInDevelop() 76 | { 77 | (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'app'); 78 | $app = (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'app'); 79 | $this->assertInstanceOf(AppInterface::class, $app); 80 | /** @var Dep $dep */ 81 | $dep = $app->resource->uri('page://self/dep')(); 82 | $this->assertInstanceOf(FakeDep::class, $dep->depInterface); 83 | $this->assertInstanceOf(FakeDep::class, $dep->dep); 84 | } 85 | 86 | public function testSerializeApp() 87 | { 88 | $app = (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'prod-app'); 89 | $this->assertInstanceOf(AbstractApp::class, unserialize(serialize($app))); 90 | } 91 | 92 | public function testCompileOnDemandInDevelopment() 93 | { 94 | (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'app'); 95 | $app = (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'app'); 96 | $this->assertInstanceOf(AppInterface::class, $app); 97 | /** @var Dep $dep */ 98 | $dep = $app->resource->uri('page://self/dep')(); 99 | $this->assertInstanceOf(FakeDep::class, $dep->depInterface); 100 | $this->assertInstanceOf(FakeDep::class, $dep->dep); 101 | } 102 | 103 | public function testCompileOnDemandInProduction() 104 | { 105 | (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'prod-app'); 106 | $app = (new Bootstrap)->getApp('FakeVendor\HelloWorld', 'prod-app'); 107 | $this->assertInstanceOf(AppInterface::class, $app); 108 | /** @var Dep $dep */ 109 | $dep = $app->resource->uri('page://self/dep')(); 110 | $this->assertInstanceOf(FakeDep::class, $dep->depInterface); 111 | $this->assertInstanceOf(FakeDep::class, $dep->dep); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src-deprecated/Compiler/CompileApp.php: -------------------------------------------------------------------------------- 1 | 0, 30 | 'method' => 0, 31 | 'time' => 0, 32 | ]; 33 | 34 | /** 35 | * Compile application 36 | * 37 | * DI+AOP script file 38 | * Parameter meta information 39 | * (No annotation cached) 40 | * 41 | * @param list $extraContexts 42 | * 43 | * @return array{class: int, method: int, time: float} 44 | */ 45 | public function compile(CompileInjector $injector, array $extraContexts = []): array 46 | { 47 | $start = microtime(true); 48 | $reader = $injector->getInstance(Reader::class); 49 | assert($reader instanceof Reader); 50 | $namedParams = $injector->getInstance(NamedParameterInterface::class); 51 | assert($namedParams instanceof NamedParameterInterface); 52 | // create DI factory class and AOP compiled class for all resources and save $app cache. 53 | $app = $injector->getInstance(AppInterface::class); 54 | assert($app instanceof AppInterface); 55 | $meta = $injector->getInstance(AbstractAppMeta::class); 56 | // check resource injection and create annotation cache 57 | $resources = $meta->getResourceListGenerator(); 58 | foreach ($resources as $resource) { 59 | $this->logs['class']++; 60 | [$className] = $resource; 61 | $this->saveMeta($namedParams, new ReflectionClass($className)); 62 | } 63 | 64 | $this->compileExtraContexts($extraContexts, $meta); 65 | $this->logs['time'] = (float) sprintf('%.3f', microtime(true) - $start); 66 | 67 | return $this->logs; 68 | } 69 | 70 | /** 71 | * Save annotation and method meta information 72 | * 73 | * @param ReflectionClass $class 74 | */ 75 | private function saveMeta(NamedParameterInterface $namedParams, ReflectionClass $class): void 76 | { 77 | $instance = $class->newInstanceWithoutConstructor(); 78 | 79 | $methods = $class->getMethods(); 80 | foreach ($methods as $method) { 81 | $methodName = $method->getName(); 82 | 83 | if (! str_starts_with($methodName, 'on')) { 84 | continue; 85 | } 86 | 87 | $this->logs['method']++; 88 | 89 | $this->saveNamedParam($namedParams, $instance, $methodName); 90 | } 91 | } 92 | 93 | private function saveNamedParam(NamedParameterInterface $namedParameter, object $instance, string $method): void 94 | { 95 | // named parameter 96 | if (! in_array($method, ['onGet', 'onPost', 'onPut', 'onPatch', 'onDelete', 'onHead'], true)) { 97 | return; // @codeCoverageIgnore 98 | } 99 | 100 | $callable = [$instance, $method]; 101 | if (! is_callable($callable)) { 102 | return; // @codeCoverageIgnore 103 | } 104 | 105 | try { 106 | $namedParameter->getParameters($callable, []); 107 | // @codeCoverageIgnoreStart 108 | } catch (ParameterException) { 109 | return; // It is OK to ignore exceptions. The objective is to obtain meta-information. 110 | 111 | // @codeCoverageIgnoreEnd 112 | } 113 | } 114 | 115 | /** @param list $extraContexts */ 116 | public function compileExtraContexts(array $extraContexts, AbstractAppMeta $meta): void 117 | { 118 | $cache = (new LocalCacheProvider())->get(); 119 | foreach ($extraContexts as $context) { 120 | $contextualMeta = new Meta($meta->name, $context, $meta->appDir); 121 | PackageInjector::getInstance($contextualMeta, $context, $cache)->getInstance(AppInterface::class); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Injector/PackageInjector.php: -------------------------------------------------------------------------------- 1 | 38 | */ 39 | private static array $instances; 40 | 41 | /** @codeCoverageIgnore */ 42 | private function __construct() 43 | { 44 | } 45 | 46 | /** 47 | * Returns an instance of InjectorInterface based on the given parameters 48 | * 49 | * @param Context $context 50 | * 51 | * - Injector instances are cached in memory and in the cache adapter. 52 | * - The injector is re-used in subsequent calls in the same context in the unit test. 53 | */ 54 | public static function getInstance(AbstractAppMeta $meta, string $context, CacheInterface|null $cache): InjectorInterface 55 | { 56 | $injectorId = str_replace('\\', '_', $meta->name) . $context; 57 | if (isset(self::$instances[$injectorId])) { 58 | return self::$instances[$injectorId]; 59 | } 60 | 61 | assert($cache instanceof AdapterInterface); 62 | /** @psalm-suppress MixedAssignment, MixedArrayAccess */ 63 | [$injector, $fileUpdate] = $cache->getItem($injectorId)->get(); // @phpstan-ignore-line 64 | $isCacheableInjector = $injector instanceof ScriptInjector || ($injector instanceof InjectorInterface && $fileUpdate instanceof FileUpdate && $fileUpdate->isNotUpdated($meta)); 65 | if (! $isCacheableInjector) { 66 | $injector = self::getInjector($meta, $context, $cache, $injectorId); 67 | } 68 | 69 | self::$instances[$injectorId] = $injector; 70 | 71 | return $injector; 72 | } 73 | 74 | /** 75 | * Return an injector instance with the given override module 76 | * 77 | * @param Context $context 78 | * 79 | * This is useful for testing purposes, where you want to override a module with a mock or stub 80 | */ 81 | public static function factory(AbstractAppMeta $meta, string $context, AbstractModule|null $overrideModule = null): InjectorInterface 82 | { 83 | $scriptDir = $meta->tmpDir . '/di'; 84 | ! is_dir($scriptDir) && ! @mkdir($scriptDir) && ! is_dir($scriptDir); 85 | $module = (new Module())($meta, $context); 86 | 87 | if ($overrideModule instanceof AbstractModule) { 88 | $module->override($overrideModule); 89 | } 90 | 91 | // Bind ResourceObject 92 | $module->install(new ResourceObjectModule($meta->getResourceListGenerator())); 93 | 94 | $injector = new RayInjector($module, $scriptDir); 95 | $isProd = $injector->getInstance('', Compile::class); 96 | assert(is_bool($isProd)); 97 | if ($isProd) { 98 | $compiler = new Compiler(); 99 | $compiler->compile($module, $scriptDir); 100 | $injector = new CompiledInjector($scriptDir); 101 | } 102 | 103 | /** @psalm-suppress InvalidArgument */ 104 | $injector->getInstance(AppInterface::class); 105 | 106 | return $injector; 107 | } 108 | 109 | /** @param Context $context */ 110 | private static function getInjector(AbstractAppMeta $meta, string $context, AdapterInterface $cache, string $injectorId): InjectorInterface 111 | { 112 | $injector = self::factory($meta, $context); 113 | $cache->save($cache->getItem($injectorId)->set([$injector, new FileUpdate($meta)])); 114 | // Check the cache 115 | if ($cache->getItem($injectorId)->get() === null) { 116 | trigger_error('Failed to verify the injector cache. See https://github.com/bearsunday/BEAR.Package/issues/418', E_USER_WARNING); 117 | } 118 | 119 | return $injector; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src-deprecated/AppInjector.php: -------------------------------------------------------------------------------- 1 | checkVersion(); 64 | $this->context = $context; 65 | $this->appMeta = $appMeta instanceof AbstractAppMeta ? $appMeta : new Meta($name, $context); 66 | $this->cacheNamespace = (string) $cacheNamespace; 67 | $scriptDir = $this->appMeta->tmpDir . '/di'; 68 | ! is_dir($scriptDir) && ! @mkdir($scriptDir) && ! is_dir($scriptDir); 69 | $this->scriptDir = $scriptDir; 70 | $appDir = $this->appMeta->tmpDir . '/app'; 71 | ! is_dir($appDir) && ! @mkdir($appDir) && ! is_dir($appDir); 72 | touch($appDir . '/.do_not_clear'); 73 | $this->appDir = $appDir; 74 | $this->injector = new ScriptInjector($this->scriptDir, function () { 75 | return $this->getModule(); 76 | }); 77 | if ($cacheNamespace === null) { 78 | $this->clear(); 79 | } 80 | } 81 | 82 | /** 83 | * {inheritdoc} 84 | * 85 | * @param string $interface 86 | * @param string $name 87 | * 88 | * @return mixed 89 | */ 90 | public function getInstance($interface, $name = Name::ANY) 91 | { 92 | return $this->injector->getInstance($interface, $name); 93 | } 94 | 95 | /** 96 | * @return mixed 97 | */ 98 | public function getOverrideInstance(AbstractModule $module, string $interface, string $name = Name::ANY) 99 | { 100 | $appModule = clone $this->getModule(); 101 | $appModule->override($module); 102 | 103 | return (new Injector($appModule, $this->scriptDir))->getInstance($interface, $name); 104 | } 105 | 106 | public function clear() : void 107 | { 108 | if ((new Unlink)->once($this->appMeta->tmpDir)) { 109 | return; 110 | } 111 | $diDir = $this->appMeta->tmpDir . '/di'; 112 | ! is_dir($diDir) && ! @mkdir($diDir) && ! is_dir($diDir); 113 | file_put_contents($this->scriptDir . ScriptInjector::MODULE, serialize($this->getModule())); 114 | } 115 | 116 | /** 117 | * @return mixed 118 | */ 119 | public function getCachedInstance(string $interface, string $name = Name::ANY) 120 | { 121 | $cache = new FilesystemCache($this->appDir); 122 | $id = $interface . $name . $this->context . $this->cacheNamespace; 123 | $instance = $cache->fetch($id); 124 | if ($instance) { 125 | return $instance; 126 | } 127 | $instance = $this->injector->getInstance($interface, $name); 128 | $cache->save($id, $instance); 129 | 130 | return $instance; 131 | } 132 | 133 | private function getModule() : AbstractModule 134 | { 135 | if ($this->module instanceof AbstractModule) { 136 | return $this->module; 137 | } 138 | $module = (new Module)($this->appMeta, $this->context); 139 | $module->install(new CacheModule()); 140 | /* @var AbstractModule $module */ 141 | $container = $module->getContainer(); 142 | (new Bind($container, InjectorInterface::class))->toInstance($this->injector); 143 | (new Bind($container, ''))->annotatedWith('cache_namespace')->toInstance($this->cacheNamespace); 144 | $this->module = $module; 145 | 146 | return $module; 147 | } 148 | 149 | private function checkVersion(): void 150 | { 151 | if (! class_exists('Doctrine\Common\Cache\FilesystemCache')) { 152 | throw new RuntimeException('Doctrine cache ^1.0 is required for AppInjector. Please install doctrine/cache ^1.0'); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Compiler/CompileAutoload.php: -------------------------------------------------------------------------------- 1 | overwritten, true)) { 60 | return $filename . ' (overwritten)'; 61 | } 62 | 63 | return $filename; 64 | } 65 | 66 | public function __invoke(): int 67 | { 68 | ($this->fakeRun)(); 69 | /** @var list $classes */ 70 | $classes = (array) $this->classes; 71 | $paths = $this->getPaths($classes); 72 | $autoload = $this->saveAutoloadFile($this->appMeta->appDir, $paths); 73 | $start = $_SERVER['REQUEST_TIME_FLOAT'] ?? 0; 74 | assert(is_float($start)); 75 | $time = number_format(microtime(true) - $start, 2); 76 | $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3); 77 | printf("Compilation (2/2) took %f seconds and used %fMB of memory\n", $time, $memory); 78 | printf("autoload.php: %s\n", $this->getFileInfo($autoload)); 79 | 80 | return 0; 81 | } 82 | 83 | /** 84 | * @param list $classes 85 | * 86 | * @return ClassPaths 87 | */ 88 | public function getPaths(array $classes): array 89 | { 90 | $paths = []; 91 | foreach ($classes as $class) { 92 | // could be phpdoc tag by annotation loader 93 | if ($this->isNotAutoloadble($class)) { 94 | continue; 95 | } 96 | 97 | /** @var class-string $class */ 98 | $filePath = (string) (new ReflectionClass($class))->getFileName(); 99 | if (! $this->isNotCompileFile($filePath)) { 100 | continue; // @codeCoverageIgnore 101 | } 102 | 103 | $paths[] = $this->getRelativePath($this->appDir, $filePath); 104 | } 105 | 106 | return $paths; 107 | } 108 | 109 | /** 110 | * @param AppDir $appDir 111 | * @param ClassPaths $paths 112 | */ 113 | public function saveAutoloadFile(string $appDir, array $paths): string 114 | { 115 | $requiredFile = ''; 116 | foreach ($paths as $path) { 117 | $requiredFile .= sprintf( 118 | "require %s;\n", 119 | $path, 120 | ); 121 | } 122 | 123 | $autoloadFile = sprintf("context, $requiredFile); 130 | $appDirRealpath = realpath($appDir); 131 | assert($appDirRealpath !== false); 132 | $fileName = $appDirRealpath . '/autoload.php'; 133 | 134 | ($this->filePutContents)($fileName, $autoloadFile); 135 | 136 | return $fileName; 137 | } 138 | 139 | private function isNotAutoloadble(string $class): bool 140 | { 141 | return ! class_exists($class, false) && ! interface_exists($class, false) && ! trait_exists($class, false); 142 | } 143 | 144 | private function isNotCompileFile(string $filePath): bool 145 | { 146 | return file_exists($filePath) || is_int(strpos($filePath, 'phar')); 147 | } 148 | 149 | private function getRelativePath(string $rootDir, string $file): string 150 | { 151 | $dir = (string) realpath($rootDir); 152 | if (str_contains($file, $dir)) { 153 | return (string) preg_replace('#^' . preg_quote($dir, '#') . '#', "__DIR__ . '", $file) . "'"; 154 | } 155 | 156 | return sprintf("'%s'", $file); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Provide/Router/HttpMethodParams.php: -------------------------------------------------------------------------------- 1 | stdIn = $stdIn; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | #[Override] 51 | public function get(array $server, array $get, array $post) 52 | { 53 | // set the original value 54 | $method = strtolower($server['REQUEST_METHOD']); 55 | 56 | // early return on GET or HEAD 57 | if ($method === 'get' || $method === 'head') { 58 | return [$method, $get]; 59 | } 60 | 61 | return $this->unsafeMethod($method, $server, $post); 62 | } 63 | 64 | /** 65 | * @param array{HTTP_X_HTTP_METHOD_OVERRIDE?: string, ...} $server 66 | * @param QueryParams $post 67 | * 68 | * @return array{0: string, 1: QueryParams} 69 | */ 70 | // phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamName 71 | private function unsafeMethod(string $method, array $server, array $post): array 72 | { 73 | /** @var array{_method?: string} $params */ 74 | $params = $this->getParams($method, $server, $post); 75 | 76 | if ($method === 'post') { 77 | [$method, $params] = $this->getOverrideMethod($method, $server, $params); 78 | } 79 | 80 | return [$method, $params]; 81 | } 82 | 83 | /** 84 | * @param array{HTTP_X_HTTP_METHOD_OVERRIDE?: string, ...} $server 85 | * @param array{_method?: string} $params 86 | * 87 | * @return array{0: string, 1: QueryParams} 88 | */ 89 | // phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamName 90 | private function getOverrideMethod(string $method, array $server, array $params): array 91 | { 92 | // must be a POST to do an override 93 | 94 | // look for override in post data 95 | if (isset($params['_method'])) { 96 | $method = strtolower($params['_method']); 97 | unset($params['_method']); 98 | 99 | return [$method, $params]; 100 | } 101 | 102 | // look for override in headers 103 | if (isset($server['HTTP_X_HTTP_METHOD_OVERRIDE'])) { 104 | $method = strtolower($server['HTTP_X_HTTP_METHOD_OVERRIDE']); 105 | } 106 | 107 | return [$method, $params]; 108 | } 109 | 110 | /** 111 | * Return request parameters 112 | * 113 | * @param array{CONTENT_TYPE?: string, HTTP_CONTENT_TYPE?: string, ...} $server 114 | * @param QueryParams $post 115 | * 116 | * @return QueryParams 117 | */ 118 | // phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamName 119 | private function getParams(string $method, array $server, array $post): array 120 | { 121 | // post data exists 122 | if ($method === 'post' && ! empty($post)) { 123 | return $post; 124 | } 125 | 126 | if (in_array($method, ['post', 'put', 'patch', 'delete'], true)) { 127 | return $this->phpInput($server); 128 | } 129 | 130 | return $post; 131 | } 132 | 133 | /** 134 | * Return request query by media-type 135 | * 136 | * @param array{CONTENT_TYPE?: string, HTTP_CONTENT_TYPE?: string, ...} $server $_SERVER 137 | * 138 | * @return QueryParams 139 | */ 140 | // phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamName 141 | private function phpInput(array $server): array 142 | { 143 | $contentType = $server[self::CONTENT_TYPE] ?? $server[self::HTTP_CONTENT_TYPE] ?? ''; 144 | $isFormUrlEncoded = str_contains($contentType, self::FORM_URL_ENCODE); 145 | if ($isFormUrlEncoded) { 146 | parse_str(rtrim($this->getRawBody($server)), $put); 147 | 148 | /** @var QueryParams $put */ 149 | return $put; 150 | } 151 | 152 | $isApplicationJson = str_contains($contentType, self::APPLICATION_JSON); 153 | if (! $isApplicationJson) { 154 | return []; 155 | } 156 | 157 | /** @var QueryParams $content */ 158 | $content = json_decode($this->getRawBody($server), true); 159 | $error = json_last_error(); 160 | if ($error !== JSON_ERROR_NONE) { 161 | throw new InvalidRequestJsonException(json_last_error_msg()); 162 | } 163 | 164 | return $content; 165 | } 166 | 167 | /** @param array{HTTP_RAW_POST_DATA?: string, ...} $server */ 168 | // phpcs:ignore Squiz.Commenting.FunctionComment.MissingParamName 169 | private function getRawBody(array $server): string 170 | { 171 | return $server['HTTP_RAW_POST_DATA'] ?? rtrim((string) file_get_contents($this->stdIn)); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Provide/Router/CliRouter.php: -------------------------------------------------------------------------------- 1 | , 38 | * REQUEST_URI: string, 39 | * REQUEST_METHOD: string, 40 | * CONTENT_TYPE?: string, 41 | * HTTP_CONTENT_TYPE?: string, 42 | * HTTP_RAW_POST_DATA?: string 43 | * } 44 | */ 45 | final class CliRouter implements RouterInterface 46 | { 47 | private Stdio $stdIo; 48 | private Throwable|null $terminateException = null; 49 | 50 | public function __construct( 51 | #[Named('original')] 52 | private RouterInterface $router, 53 | Stdio|null $stdIo = null, 54 | #[StdIn] 55 | private string $stdIn = '', 56 | ) { 57 | $this->stdIo = $stdIo ?: (new CliFactory())->newStdio(); 58 | } 59 | 60 | public function __destruct() 61 | { 62 | file_exists($this->stdIn) && unlink($this->stdIn); 63 | } 64 | 65 | public function __wakeup(): void 66 | { 67 | $this->stdIo = (new CliFactory())->newStdio(); 68 | } 69 | 70 | public function setTerminateException(Throwable $e): void 71 | { 72 | $this->terminateException = $e; 73 | } 74 | 75 | /** 76 | * @psalm-api 77 | * @deprecated Use constructor injection 78 | * @codeCoverageIgnore 79 | */ 80 | public function setStdIn( 81 | string $stdIn, 82 | ): void { 83 | $this->stdIn = $stdIn; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | * 89 | * @param Globals $globals 90 | * @param Server $server 91 | */ 92 | #[Override] 93 | public function match(array $globals, array $server) 94 | { 95 | /** @var CliServer $server */ 96 | $this->validateArgs($server['argc'], $server['argv']); 97 | // covert console $_SERVER to web $_SERVER $GLOBALS 98 | /** @psalm-suppress InvalidArgument */ 99 | [$method, $query, $server] = $this->parseServer($server); 100 | /** @psalm-suppress MixedArgumentTypeCoercion */ 101 | [$webGlobals, $webServer] = $this->addQuery($method, $query, $globals, $server); 102 | 103 | return $this->router->match($webGlobals, $webServer); 104 | } 105 | 106 | /** 107 | * {@inheritDoc} 108 | */ 109 | #[Override] 110 | public function generate($name, $data) 111 | { 112 | return $this->router->generate($name, $data); 113 | } 114 | 115 | /** 116 | * Set user input query to $globals or &$server 117 | * 118 | * @param QueryParams $query 119 | * @param Globals $globals 120 | * @param Server $server 121 | * 122 | * @return array{0:Globals, 1:Server} 123 | */ 124 | private function addQuery(string $method, array $query, array $globals, array $server): array 125 | { 126 | if ($method === 'get') { 127 | $globals['_GET'] = $query; 128 | 129 | return [$globals, $server]; 130 | } 131 | 132 | if ($method === 'post') { 133 | $globals['_POST'] = $query; 134 | 135 | return [$globals, $server]; 136 | } 137 | 138 | $server = $this->getStdIn($method, $query, $server); 139 | 140 | return [$globals, $server]; 141 | } 142 | 143 | private function error(string $command): void 144 | { 145 | $help = new CliRouterHelp(new OptionFactory()); 146 | $this->stdIo->outln($help->getHelp($command)); 147 | } 148 | 149 | /** @SuppressWarnings(PHPMD) */ 150 | private function terminate(int $status): void 151 | { 152 | if ($this->terminateException instanceof Exception) { 153 | throw $this->terminateException; 154 | } 155 | 156 | // @codeCoverageIgnoreStart 157 | exit($status); 158 | } 159 | 160 | // @codeCoverageIgnoreEnd 161 | 162 | /** 163 | * Return StdIn in PUT, PATCH or DELETE 164 | * 165 | * @param QueryParams $query 166 | * @param Server $server 167 | * 168 | * @return Server 169 | */ 170 | private function getStdIn(string $method, array $query, array $server): array 171 | { 172 | if ($method === 'put' || $method === 'patch' || $method === 'delete') { 173 | $server[HttpMethodParams::CONTENT_TYPE] = HttpMethodParams::FORM_URL_ENCODE; 174 | file_put_contents($this->stdIn, http_build_query($query)); 175 | 176 | return $server; 177 | } 178 | 179 | return $server; 180 | } 181 | 182 | /** @param array $argv */ 183 | private function validateArgs(int $argc, array $argv): void 184 | { 185 | if ($argc >= 3) { 186 | return; 187 | } 188 | 189 | $this->error(basename($argv[0])); 190 | $this->terminate(Status::USAGE); 191 | // @codeCoverageIgnoreStart 192 | } 193 | 194 | // @codeCoverageIgnoreEnd 195 | 196 | /** 197 | * Return $method, $query, $server from $server 198 | * 199 | * @param Server $server 200 | * 201 | * @return array{string, QueryParams, Server} 202 | */ 203 | private function parseServer(array $server): array 204 | { 205 | /** @var array{argv: array} $server */ 206 | [, $method, $uri] = $server['argv']; 207 | $urlQuery = (string) parse_url($uri, PHP_URL_QUERY); 208 | $urlPath = (string) parse_url($uri, PHP_URL_PATH); 209 | $query = []; 210 | if ($urlQuery !== '') { 211 | parse_str($urlQuery, $query); 212 | } 213 | 214 | $server = [ 215 | 'REQUEST_METHOD' => strtoupper($method), 216 | 'REQUEST_URI' => $urlPath, 217 | ]; 218 | 219 | /** @var QueryParams $query */ 220 | return [$method, $query, $server]; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | BEAR.Package is the core framework implementation package for BEAR.Sunday, a resource-oriented PHP framework. It provides dependency injection, AOP compilation, routing, error handling, and context-based module loading. 8 | 9 | ## Development Commands 10 | 11 | ### Testing 12 | ```bash 13 | # Run all tests 14 | composer test 15 | # Or directly 16 | ./vendor/bin/phpunit 17 | 18 | # Run specific test suites 19 | ./vendor/bin/phpunit --testsuite=core 20 | ./vendor/bin/phpunit --testsuite=context 21 | ./vendor/bin/phpunit --testsuite=provide 22 | 23 | # Run single test file 24 | ./vendor/bin/phpunit tests/SomeTest.php 25 | 26 | # Run single test method 27 | ./vendor/bin/phpunit --filter testMethodName tests/SomeTest.php 28 | 29 | # Coverage (with pcov) 30 | composer pcov 31 | 32 | # Coverage (with xdebug) 33 | composer coverage 34 | ``` 35 | 36 | ### Code Quality 37 | ```bash 38 | # Run all quality checks 39 | composer tests 40 | 41 | # Code style check 42 | composer cs 43 | 44 | # Fix code style 45 | composer cs-fix 46 | 47 | # Static analysis 48 | composer sa 49 | 50 | # Clean caches 51 | composer clean 52 | ``` 53 | 54 | ### Compilation 55 | ```bash 56 | # Compile a BEAR.Sunday application 57 | ./bin/bear.compile 'VendorName\AppName' context-name /path/to/app 58 | 59 | # Example (used in tests) 60 | composer compile 61 | ``` 62 | 63 | ## Architecture 64 | 65 | ### Type Definitions 66 | 67 | Domain types are defined in `src/Types.php` using Psalm type aliases: 68 | - `AppName`: Application namespace (e.g., `"MyVendor\\MyApp"`) 69 | - `Context`: Context string (e.g., `"prod-api-app"`) 70 | - `AppDir`: Application directory path 71 | 72 | These types are imported via `@psalm-import-type` throughout the codebase. 73 | 74 | ### Core Components 75 | 76 | **Injector System** 77 | - `Injector::getInstance()`: Creates/retrieves cached DI container for an application 78 | - `PackageInjector`: Internal injector implementation with caching support 79 | - Uses Ray.Di for dependency injection with compile-time optimization 80 | 81 | **Compiler** 82 | - Compilation process that generates optimized autoload, preload, DI scripts, and object graphs 83 | - `Compiler::compile()`: Main compilation orchestrator 84 | - Sub-compilers: `CompileAutoload`, `CompilePreload`, `CompileObjectGraph`, `CompileClassMetaInfo` 85 | - Tracks loaded classes during compilation for preload generation 86 | - Uses Ray.Compiler to compile DI container to PHP scripts 87 | 88 | **Module System** 89 | - `Module` class: Context-based module loader that processes hyphenated contexts (e.g., "prod-api-app") 90 | - Context modules are loaded in reverse order (right-to-left) 91 | - Looks for modules in app namespace first, falls back to `BEAR\Package\Context\` 92 | - Built-in contexts: `ProdModule`, `ApiModule`, `CliModule`, `HalModule` 93 | - `AbstractAppModule`: Base class for app modules that need `AbstractAppMeta` injection 94 | 95 | **PackageModule** 96 | - Base framework module that installs core functionality 97 | - Installs: QueryRepository, WebRouter, VndError, PsrLogger, Stream, CreatedResource, DiCompile, Sunday modules 98 | - Override pattern with `Psr6NullModule` for cache configuration 99 | 100 | ### Directory Structure 101 | 102 | ``` 103 | src/ 104 | ├── Annotation/ # Framework annotations (@ReturnCreatedResource, @StdIn) 105 | ├── Compiler/ # Compilation components 106 | ├── Context/ # Built-in context modules (Prod, Api, Cli, Hal) 107 | ├── Exception/ # Framework exceptions 108 | ├── Injector/ # DI container implementation 109 | ├── Module/ # Core modules and providers 110 | ├── Provide/ 111 | │ ├── Error/ # Error handling (DevVndErrorPage, ProdVndErrorPage, ErrorHandler) 112 | │ ├── Logger/ # PSR-3 logger integration (Monolog) 113 | │ ├── Representation/ # Resource representation (CreatedResource) 114 | │ ├── Router/ # Routers (WebRouter, CliRouter, RouterCollection) 115 | │ └── Transfer/ # Response transfer (CliResponder) 116 | ├── AbstractAppModule.php 117 | ├── Compiler.php 118 | ├── Injector.php 119 | ├── Module.php 120 | └── PackageModule.php 121 | 122 | src-deprecated/ # Deprecated code for BC 123 | tests/Fake/ # Test fixtures and fake applications 124 | ``` 125 | 126 | ### Key Patterns 127 | 128 | **Context-Based Configuration** 129 | - Applications can have multiple contexts (e.g., "prod-api-app", "dev-cli-app") 130 | - Each context segment maps to a Module (AppModule, ApiModule, ProdModule) 131 | - Modules are installed right-to-left, allowing progressive overrides 132 | 133 | **Dependency Injection** 134 | - Constructor injection with type hints 135 | - Interface binding in modules 136 | - Provider classes for complex object creation 137 | - AOP interceptors via `bindInterceptor()` 138 | 139 | **Resource-Oriented** 140 | - Resources are the primary abstraction (not MVC) 141 | - Two resource types: 142 | - `Page` resources: Web page endpoints (like controllers) 143 | - `App` resources: Internal application resources (like API endpoints) 144 | - Resource classes extend `ResourceObject` and use HTTP method handlers: 145 | - `onGet()`, `onPost()`, `onPut()`, `onPatch()`, `onDelete()` 146 | - Resources set `$this->body` and return `$this` 147 | - `@Link` or `#[Link]` attributes define hypermedia relations 148 | - Router maps HTTP/CLI requests to resource URIs 149 | - Error responses use vnd.error media type 150 | 151 | **Routing** 152 | - `WebRouter`: Converts HTTP requests to resource URIs (scheme + host + path) 153 | - `CliRouter`: Converts CLI arguments to resource URIs 154 | - CLI format: `php public/index.php {method} {uri} [query params]` 155 | - Example: `php public/index.php get /user?id=1` 156 | - Wraps another router and converts CLI context to web context 157 | - Both routers implement `RouterInterface` 158 | 159 | **Compilation for Performance** 160 | - DI container compiled to PHP code 161 | - Preload file generation for opcache 162 | - Autoload optimization 163 | - Object graph visualization (DOT format) 164 | 165 | ## Test Applications 166 | 167 | The `tests/Fake/` directory contains example applications: 168 | - `fake-app`: Full-featured test application with resources, modules, providers 169 | - `fake-min-app`: Minimal application structure 170 | - `import-app`: Tests module import functionality 171 | 172 | These serve as both test fixtures and reference implementations. 173 | 174 | ## PHP Requirements 175 | 176 | - PHP 8.2+ 177 | - Extensions: hash 178 | - Ray.Di for dependency injection 179 | - Ray.Aop for aspect-oriented programming 180 | - PHP 8 attributes support (annotations deprecated) 181 | 182 | ## CI/CD 183 | 184 | - GitHub Actions workflow: `.github/workflows/continuous-integration.yml` 185 | - Uses shared Ray.Di workflow from `ray-di/.github` 186 | - Tests against PHP 8.1, 8.2, 8.3, 8.4 187 | - Runs on push, pull requests, and manual workflow dispatch 188 | 189 | ## Important Notes 190 | 191 | - Active development branch: `1.x` 192 | - `src-deprecated/` contains backward compatibility code 193 | - Tests use PSR-4 autoloading for fake applications 194 | - Compiler generates files in `var/` directory (gitignored) 195 | -------------------------------------------------------------------------------- /src/Compiler.php: -------------------------------------------------------------------------------- 1 | */ 46 | private ArrayObject $classes; 47 | private Meta $appMeta; 48 | private CompileAutoload $dumpAutoload; 49 | private CompilePreload $compilePreload; 50 | private CompileObjectGraph $compilerObjectGraph; 51 | 52 | /** 53 | * @param AppName $appName application name "MyVendor|MyProject" 54 | * @param Context $context application context "prod-app" 55 | * @param AppDir $appDir application path 56 | * 57 | * @SuppressWarnings(PHPMD.BooleanArgumentFlag) 58 | */ 59 | public function __construct(string $appName, private string $context, string $appDir, bool $prepend = true) 60 | { 61 | /** @var ArrayObject $classes */ 62 | $classes = new ArrayObject(); 63 | $this->classes = $classes; 64 | $this->registerLoader($appDir, $prepend); 65 | $this->hookNullObjectClass($appDir); 66 | $this->appMeta = new Meta($appName, $context, $appDir); 67 | /** @psalm-suppress MixedAssignment (?) */ 68 | $injector = Injector::getInstance($appName, $context, $appDir); 69 | /** @var ArrayObject $overWritten */ 70 | $overWritten = new ArrayObject(); 71 | $filePutContents = new FilePutContents($overWritten); 72 | $fakeRun = new FakeRun($injector, $context, $this->appMeta); 73 | $this->dumpAutoload = new CompileAutoload($fakeRun, $filePutContents, $this->appMeta, $overWritten, $this->classes, $appDir, $context); 74 | $this->compilePreload = new CompilePreload($fakeRun, $this->dumpAutoload, $filePutContents, $classes, $context); 75 | $this->compilerObjectGraph = new CompileObjectGraph($filePutContents, $this->appMeta->logDir); 76 | } 77 | 78 | /** 79 | * Compile application 80 | * 81 | * @return 0|1 exit code 82 | */ 83 | public function compile(): int 84 | { 85 | $preload = ($this->compilePreload)($this->appMeta, $this->context); 86 | $module = (new Module())($this->appMeta, $this->context); 87 | $compiler = new \Ray\Compiler\Compiler(); 88 | $appDirRealpath = realpath($this->appMeta->appDir); 89 | assert($appDirRealpath !== false); 90 | $scriptDir = $appDirRealpath . '/var/di/' . $this->context; 91 | $compiler->compile($module, $scriptDir); 92 | 93 | // Compile class meta info (annotations and named parameters) 94 | $compiled = $this->compileClassMetaInfo(); 95 | 96 | echo PHP_EOL; 97 | $dot = ($this->compilerObjectGraph)($module); 98 | $start = $_SERVER['REQUEST_TIME_FLOAT'] ?? 0.0; 99 | $time = number_format(microtime(true) - $start, 2); 100 | $memory = number_format(memory_get_peak_usage() / (1024 * 1024), 3); 101 | echo PHP_EOL; 102 | printf("Compilation took %f seconds and used %fMB of memory\n", $time, $memory); 103 | printf("Compiled: %d resource classes\n", $compiled); 104 | printf("Preload compile: %s\n", $this->dumpAutoload->getFileInfo($preload)); 105 | $dotRealpath = realpath($dot); 106 | assert($dotRealpath !== false); 107 | printf("Object graph diagram: %s\n", $dotRealpath); 108 | 109 | return 0; 110 | } 111 | 112 | public function dumpAutoload(): int 113 | { 114 | return ($this->dumpAutoload)(); 115 | } 116 | 117 | private function compileClassMetaInfo(): int 118 | { 119 | $injector = Injector::getInstance($this->appMeta->name, $this->context, $this->appMeta->appDir); 120 | $namedParams = $injector->getInstance(NamedParameterInterface::class); 121 | assert($namedParams instanceof NamedParameterInterface); 122 | 123 | $compileClassMetaInfo = new CompileClassMetaInfo(); 124 | $resources = $this->appMeta->getResourceListGenerator(); 125 | $count = 0; 126 | foreach ($resources as $resource) { 127 | [$className] = $resource; 128 | $compileClassMetaInfo($namedParams, $className); 129 | $count++; 130 | } 131 | 132 | return $count; 133 | } 134 | 135 | /** @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ 136 | private function registerLoader(string $appDir, bool $prepend = true): void 137 | { 138 | $this->unregisterComposerLoader(); 139 | $loaderFile = $appDir . '/vendor/autoload.php'; 140 | if (! file_exists($loaderFile)) { 141 | throw new RuntimeException('no loader'); 142 | } 143 | 144 | $loader = require $loaderFile; 145 | assert($loader instanceof ClassLoader); 146 | spl_autoload_register( 147 | /** @ class-string $class */ 148 | function (string $class) use ($loader): void { 149 | $loader->loadClass($class); 150 | if ( 151 | $class === NullPage::class 152 | || is_int(strpos($class, Compiler::class)) 153 | || is_int(strpos($class, NullPage::class)) 154 | ) { 155 | return; 156 | } 157 | 158 | /** @psalm-suppress NullArgument */ 159 | $this->classes[] = $class; 160 | }, 161 | true, 162 | $prepend, 163 | ); 164 | } 165 | 166 | private function hookNullObjectClass(string $appDir): void 167 | { 168 | $appDirRealpath = realpath($appDir); 169 | assert($appDirRealpath !== false); 170 | $compileScript = $appDirRealpath . '/.compile.php'; 171 | if (! file_exists($compileScript)) { 172 | // @codeCoverageIgnoreStart 173 | return; 174 | // @codeCoverageIgnoreEnd 175 | } 176 | 177 | require $compileScript; 178 | } 179 | 180 | private function unregisterComposerLoader(): void 181 | { 182 | $autoload = spl_autoload_functions(); 183 | if (! isset($autoload[0])) { 184 | // @codeCoverageIgnoreStart 185 | return; 186 | // @codeCoverageIgnoreEnd 187 | } 188 | 189 | spl_autoload_unregister($autoload[0]); 190 | } 191 | } 192 | --------------------------------------------------------------------------------