As backward compatibility is maintained, each package can be updated independently. Moreover, there is no version number that locks the entire system, as seen in other frameworks, and there is no mechanism for object proxies that hold cross-cutting dependencies between objects.
The Acyclic Dependencies Principle is in harmony with the DI (Dependency Injection) principle, and the root object generated during the bootstrapping process of BEAR.Sunday is also constructed following the structure of this Acyclic Dependencies Principle.
[
](/images/screen/clean-architecture.png)
The same applies to the runtime. When accessing a resource, first, the cross-cutting processing of the AOP aspects bound to the method is executed, and then the method determines the state of the resource. At this point, the method is not aware of the existence of the aspects bound to it. The same goes for resources embedded in the resource's state. They do not have knowledge of the outer layers or elements. The separation of concerns is clearly defined.
### Code Quality
To provide applications with high code quality, the BEAR.Sunday framework also strives to maintain a high standard of code quality.
* The framework code is applied at the strictest level by both static analysis tools, Psalm and PHPStan.
* It maintains 100% test coverage and nearly 100% type coverage.
* It is fundamentally an immutable system and is so clean that initialization is not required every time, even in tests. It unleashes the power of PHP's asynchronous communication engines like Swoole.
### Architecture-Enabled Security Analysis
BEAR.Sunday's architecture fundamentally simplifies security analysis.
Every endpoint is a ResourceObject with explicit `onGet` and `onPost` methods, inputs are declared via JSON Schema, and dependencies are explicit through constructor injection. With no hidden magic or global state, static analysis tools can trace complete data flows.
This declarative architecture enables multi-layered security scanning combining SAST (static analysis), DAST (dynamic testing), taint analysis, and AI auditing. Framework-aware specialized tools detect vulnerabilities that generic tools cannot find.
## The Value BEAR.Sunday Brings
### Value for Developers
* Improved productivity: Based on robust design patterns and principles with constraints that don't change over time, developers can focus on core business logic.
* Collaboration in teams: By providing development teams with consistent guidelines and structure, it keeps the code of different engineers loosely coupled and unified, improving code readability and maintainability.
* Flexibility and extensibility: BEAR.Sunday's policy of not including libraries brings developers flexibility and freedom in component selection.
* Ease of testing: BEAR.Sunday's DI (Dependency Injection) and ROA (Resource Oriented Architecture) increase the ease of testing.
### Value for Users
* High performance: BEAR.Sunday's runtime optimization and CDN-centric caching strategy brings users a fast and responsive experience.
* Reliability and availability: BEAR.Sunday's CDN-centric caching strategy minimizes single points of failure (SPOF), allowing users to enjoy stable services.
* Ease of use: BEAR.Sunday's excellent connectivity makes it easy to collaborate with other languages and systems.
### Value for Business
* Reduced development costs: The consistent guidelines and structure provided by BEAR.Sunday promote a sustainable and efficient development process, reducing development costs.
* Reduced maintenance costs: BEAR.Sunday's approach to maintaining backward compatibility increases technical continuity and minimizes the time and cost of change response.
* High extensibility: With technologies like DI (Dependency Injection) and AOP (Aspect Oriented Programming) that change behavior while minimizing code changes, BEAR.Sunday allows applications to be easily extended in line with business growth and changes.
* Excellent User Experience (UX): BEAR.Sunday provides high performance and high availability, increasing user satisfaction, enhancing customer loyalty, expanding the customer base, and contributing to business success.
### Summary
Excellent constraints do not change. The constraints brought by BEAR.Sunday provide specific value to developers, users, and businesses respectively.
BEAR.Sunday is a framework designed based on the principles and spirit of the Web, providing developers with clear constraints to empower them to build flexible and robust applications.
# Version
## Supported PHP
[](https://github.com/bearsunday/BEAR.SupportedVersions/actions/workflows/continuous-integration.yml)
BEAR.Sunday supports the following supported PHP versions
* `8.1` (Old stable 25 Nov 2021 - 31 Dec 2025)
* `8.2` (Old stable 8 Dec 2022 - 31 Dec 2026)
* `8.3` (Old stable 23 Nov 2023 - 31 Dec 2027)
* `8.4` (Current stable 21 Nov 2024 - 31 Dec 2028)
* End of life ([EOL](http://php.net/eol.php))
* `5.5` (21 Jul 2016)
* `5.6` (31 Dec 2018)
* `7.0` (3 Dec 2018)
* `7.1` (1 Dec 2019)
* `7.2` (30 Nov 2020)
* `7.3` (6 Dec 2021)
* `7.4` (28 Nov 2022)
* `8.0` (26 Nov 2023)
The new optional package will be developed based on the current stable PHP. We encourage you to use the current stable PHP for quality, performance and security.
[BEAR.SupportedVersions](https://github.com/bearsunday/BEAR.SupportedVersions/), you can check the tests for each version in CI.
## Semver
BEAR.Sunday follows [Semantic Versioning](http://semver.org/lang/en/). It is not necessary to modify the application code on minor version upgrades.
`composer update` can be done at any time for packages.
## Version Policy
* The core package of the framework does not make a breaking change which requires change of user code.
* Since it does not do destructive change, it handles unnecessary old ones as `deprecated` but does not delete and new functions are always "added".
* When PHP comes to an EOL and upgraded to a major version (ex. `5.6` →` 7.0`), BEAR.Sunday will not break the BC of the application code. Even though the version number of PHP that is necessary to use the new module becomes higher, changes to the application codes are not needed.
BEAR.Sunday emphasizes clean code and **longevity**.
## Package version
The version of the framework does not lock the version of the library. The library can be updated regardless of the version of the framework.
# Quick Start
Installation is done via [composer](http://getcomposer.org)
```bash
VENDOR=MyVendor PACKAGE=MyProject composer create-project bear/skeleton my-project
cd my-project
```
Use `VENDOR` to specify the vendor name and `PACKAGE` to specify the package name for installation. If not specified, you will be prompted interactively.
Next, let's create a new resource. A resource is a class which corresponds, for instance, to a JSON payload (if working with an API-first driven model)
or a web page.
Create your own basic page resource in `src/Resource/Page/Hello.php`
```php
body = [
'greeting' => 'Hello ' . $name
];
return $this;
}
}
```
In the above example, when the page is requested using a GET method, `Hello` and the `$name` parameter (which corresponds to `$_GET['name']`) are joined, and assigned to a variable `greeting`.
The BEAR.Sunday application that you have created will work on a web server, but also in the console.
```bash
php bin/page.php get /hello
php bin/page.php get '/hello?name=World'
```
```bash
200 OK
Content-Type: application/hal+json
{
"greeting": "Hello World",
"_links": {
"self": {
"href": "/hello?name=World"
}
}
}
```
Let us fire up the php server and access our page at [http://127.0.0.1:8080/hello](http://127.0.0.1:8080/hello).
```bash
php -S 127.0.0.1:8080 -t public
```
```bash
curl -i 127.0.0.1:8080/hello
```
## Core Concepts
# Package
BEAR.Sunday application is a composer package taking BEAR.Sunday framework as dependency package.
You can also install another BEAR.Sunday application package as dependency.
## Application organization
The file layout of the BEAR.Sunday application conforms to [php-pds/skeleton](https://github.com/php-pds/skeleton) standard.
### Invoke sequence
1. Console input (`bin/app.php`, `bin/page.php`) or the web entry file (`public/index.php`) executes the `bootstrap.php` function.
2. The `$app` application object is created for the given `$context` in `bootstrap.php`.
3. The router in `$app` converts the external resource request to an internal resource request.
4. The resource request is invoked, and the resulting representation is transferred to the client.
### bootstrap/
You can access same resource through console input or web access with same boot file.
```bash
php bin/app.php options /todos // console API access (app resource)
```
```bash
php bin/page.php get '/todos?id=1' // console Web access (page resource)
```
```bash
php -S 127.0.0.1bin/app.php // PHP server
```
You can create your own boot file for different context.
### bin/
Place command-line executable files.
### src/
Place application class file.
### public/
Web public folder.
### var/
`log` and `tmp` need write permission. By default they are `var/tmp/{context}` and `var/log/{context}`.
To place them outside the project tree, pass paths to the `Meta` constructor ([Production](production.html#writable-paths)).
## Framework Package
### ray/aop
[](https://scrutinizer-ci.com/g/ray-di/Ray.Aop/)
[](https://codecov.io/gh/ray-di/Ray.Aop)
[](https://shepherd.dev/github/ray-di/Ray.Aop)
[](https://github.com/ray-di/Ray.Aop/actions/workflows/continuous-integration.yml)
An aspect oriented framework based on Java AOP Alliance API.
### ray/di
[](https://scrutinizer-ci.com/g/ray-di/Ray.Di/)
[](https://codecov.io/gh/ray-di/Ray.Di)
[](https://shepherd.dev/github/ray-di/Ray.Di)
[](https://github.com/ray-di/Ray.Di/actions/workflows/continuous-integration.yml)
A Google Guice style DI framework. It contains `ray/aop`.
### bear/resource
[](https://scrutinizer-ci.com/g/bearsunday/BEAR.Resource/?branch=1.x)
[](https://codecov.io/gh/bearsunday/BEAR.Resource)
[](https://shepherd.dev/github/bearsunday/BEAR.Resource)
[](https://github.com/bearsunday/BEAR.Resource/actions/workflows/continuous-integration.yml)
A REST framework for PHP object as a service. It contains `ray/di`.
### bear/sunday
[](https://scrutinizer-ci.com/g/bearsunday/BEAR.Sunday/?branch=1.x)
[](https://codecov.io/gh/bearsunday/BEAR.Sunday)
[](https://shepherd.dev/github/bearsunday/BEAR.Sunday)
[](https://github.com/bearsunday/BEAR.Sunday/actions/workflows/continuous-integration.yml)
A web application interface package. It contains `bear/resource`.
### bear/package
[](https://scrutinizer-ci.com/g/bearsunday/BEAR.Package/?branch=1.x)
[](https://codecov.io/gh/bearsunday/BEAR.Pacakge)
[](https://shepherd.dev/github/bearsunday/BEAR.Package)
[](https://github.com/bearsunday/BEAR.Package/actions/workflows/continuous-integration.yml)
A web application implmentation package. It contains `bear/sunday`.
## Library Package
Optional library package can be installed with `composer require` command.
| **Category** | **Composer package** | **Library**
| Router |
| |[bear/aura-router-module](https://github.com/bearsunday/BEAR.AuraRouterModule) | [Aura.Router v2](https://github.com/auraphp/Aura.Router/tree/2.x) |
| Database |
|| [ray/media-query](https://github.com/ray-di/Ray.MediaQuery) |
|| [ray/aura-sql-module](https://github.com/ray-di/Ray.AuraSqlModule) | [Aura.Sql v2](https://github.com/auraphp/Aura.Sql/tree/2.x)
|| [ray/dbal-module](https://github.com/ray-di/Ray.DbalModule) | [Doctrine DBAL](https://github.com/doctrine/dbal)
|| [ray/cake-database-module](https://github.com/ray-di/Ray.CakeDbModule) | [CakePHP v3 database](https://github.com/cakephp/database)
|| [ray/doctrine-orm-module](https://github.com/kawanamiyuu/Ray.DoctrineOrmModule) | [Doctrine ORM](https://github.com/doctrine/doctrine2)
| Storage |
||[bear/query-repository](https://github.com/bearsunday/BEAR.QueryRepository) | CQRS inspired repository
||[bear/query-module](https://github.com/ray-di/Ray.QueryModule) | Separation of external access such as DB or Web API
| Web
| |[madapaja/twig-module](http://bearsunday.github.io/manuals/1.0/ja/html.html) | [Twig](http://twig.sensiolabs.org/)
| |[ray/web-form-module](http://bearsunday.github.io/manuals/1.0/ja/form.html) | Web form
| |[ray/aura-web-module](https://github.com/Ray-Di/Ray.AuraWebModule) | [Aura.Web](https://github.com/auraphp/Aura.Web)
| |[ray/aura-session-module](https://github.com/ray-di/Ray.AuraSessionModule) | [Aura.Session](https://github.com/auraphp/Aura.Session)
| |[ray/symfony-session-module](https://github.com/kawanamiyuu/Ray.SymfonySessionModule) | [Symfony Session](https://github.com/symfony/http-foundation/tree/master/Session)
| Validation |
| |[ray/validate-module](https://github.com/ray-di/Ray.ValidateModule) | [Aura.Filter](https://github.com/auraphp/Aura.Filter)
| |[satomif/extra-aura-filter-module](https://github.com/satomif/ExtraAuraFilterModule)| [Aura.Filter](https://github.com/auraphp/Aura.Filter)
| Authorization and Authentication
| |[ray/oauth-module](https://github.com/Ray-Di/Ray.OAuthModule) | OAuth
| |[kuma-guy/jwt-auth-module](https://github.com/kuma-guy/BEAR.JwtAuthModule) | JSON Web Token
| |[ray/role-module](https://github.com/ray-di/Ray.RoleModule) | Zend Acl
| |[bear/acl-resource](https://github.com/bearsunday/BEAR.AclResource) | ACL based embedded resource
| Hypermedia
| |[kuma-guy/siren-module](https://github.com/kuma-guy/BEAR.SirenModule) | Siren
| Development
| |[ray/test-double](https://github.com/ray-di/Ray.TestDouble) | Test Double
| Asynchronous high performance |
| |[MyVendor.Swoole](https://github.com/bearsunday/MyVendor.Swoole) | [Swoole](https://github.com/swoole/swoole-src)
## Vendor Package
You can reuse common packages and tool combinations as modules with only modules and share modules of similar projects.[^1]
## Semver
All packages adhere to [Semantic Versioning](http://semver.org/).
---
[^1]: See [Koriym.DbAppPackage](https://github.com/koriym/Koriym.DbAppPackage)
# Application
## Sequence
A BEAR.Sunday app has a run order of `compile`, `request` and `response`.
### 0. Compile
An `$app` application object is created through `DI` and `AOP` configuration depending on a specified context.
An `$app` is made up of service objects as it's properties that are needed to run the application such as a `router` or `transfer` etc.
`$app` then connects these object together depending on whether it is owned by another or contains other objects.
This is called an [Object Graph](http://en.wikipedia.org/wiki/Object_graph).
`$app` is then serialized and reused in each request and response.
* router - Converting external input to resource requests
* resource - Resource client
* transfer - Output
### 1. Request
An application resource request and resource object is created based on the HTTP request.
A resource object which has methods that respond to `onGet`, `onPost` etc upon request sets the `code` or `body` property of it's own resource state.
The resource object can then `#[Embed]` or `#[Link]` other resource objects.
Methods on the resource object are only for changing the resources state and have no interest in the representation itself (HTML, JSON etc).
Before and after the method, application logic bound to the method, such as logging and authentication, is executed in AOP.
### 2. Response
A `Renderer` is injected into the resource object, then the state of resource is represented as HTML, JSON etc or however it has been configured, it is then transfered to the client.
## Boot File
To run an application, we need just two lines of code.
An entry point for a web server or console application access is usually set to `public/index.php` or `bin/app.php`.
As you can see below, we need to pass an application context to `bootstrap.php` the application script.
```php
install(new AuraSqlModule('mysql:host=localhost;dbname=test', 'username', 'password');
$this->install(new TwigModule));
// install basic module
$this->install(new PackageModule));
}
}
```
## DI bindings
`Ray.Di` is the core DI framework used in BEAR.Sunday. It binds interfaces to a class or factory to create an object graph.
```php?start_inline
// Class binding
$this->bind($interface)->to($class);
// Provider (factory) binding
$this->bind($interface)->toProvider($provider);
// Instance binding
$this->bind($interface)->toInstance($instance);
// Named binding
$this->bind($interface)->annotatedWith($annotation)->to($class);
// Singleton
$this->bind($interface)->to($class)->in(Scope::SINGLETON);
// Constructor binding
$this->bind($interface)->toConstructor($class, $named);
```
More info can be found at Ray.Di [README](https://github.com/ray-di/Ray.Di/blob/2.x/README.md)
### Binding Priority
See also: [Ray.Di Bindings](https://ray-di.github.io/manuals/1.0/en/bindings.html)
#### Within a Single Module
Bindings declared first take priority. In the following example, `Foo1` takes priority:
```php?start_inline
$this->bind(FooInterface::class)->to(Foo1::class);
$this->bind(FooInterface::class)->to(Foo2::class);
```
#### Module Installation Priority
Modules installed first take priority. In the following example, `Foo1Module` takes priority:
```php?start_inline
$this->install(new Foo1Module);
$this->install(new Foo2Module);
```
To give a later module priority, use `override()`. In the following example, `Foo2Module` takes priority:
```php?start_inline
$this->install(new Foo1Module);
$this->override(new Foo2Module);
```
#### Context String Priority
Context modules are processed in **reverse order** (right-to-left). For example, with context `prod-hal-api-app`:
```text
Installation order: AppModule → ApiModule → HalModule → ProdModule
```
Later installed modules can override earlier bindings. This means:
- `HalModule` takes priority over `AppModule`
- `ProdModule` takes priority over `HalModule`
When creating a custom context module that needs to override bindings from a built-in context (like `HalModule`), position it to the left of that context in the context string. For example, to override `HalModule`'s `RenderInterface` binding:
```php?start_inline
// Context: "prod-mycontext-hal-api-app"
// Installation order: AppModule → ApiModule → HalModule → MycontextModule → ProdModule
```
## AOP Bindings
We can "search" for classes and methods with a built-in `Matcher`, then interceptors can be bound to any found methods.
```php?start_inline
$this->bindInterceptor(
// In any class
$this->matcher->any(),
// Method(s) names that start with "delete"
$this->matcher->startWith('delete'),
// Bind a Logger interceptor
[LoggerInterceptor::class]
);
$this->bindInterceptor(
// The AdminPage class or a class inherited from it.
$this->matcher->SubclassesOf(AdminPage::class),
// Annotated with the @Auth annotation
$this->matcher->annotatedWith(Auth::class),
// Bind the AdminAuthenticationInterceptor
[AdminAuthenticationInterceptor::class]
);
```
`Matcher` has various binding methods.
* [Matcher::any](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L16) - Any
* [Matcher::annotatedWith](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L23) - Annotation
* [Matcher::subclassesOf](https://github.com/ray-di/Ray.Aop/blob/2.x/src/MatcherInterface.php#L30) - Sub class
* [Matcher::startsWith](https://github.com/ray-di/Ray.Aop/blob/2.x/src/MatcherInterface.php#L37) - start with name (class or method)
* [Matcher::logicalOr](https://github.com/ray-di/Ray.Aop/blob/2.x/src/MatcherInterface.php#L44) - OR
* [Matcher::logicalAnd](https://github.com/ray-di/Ray.Aop/blob/2.x/src/MatcherInterface.php#L51) - AND
* [Matcher::logicalNot](https://github.com/ray-di/Ray.Aop/blob/2.x/src/MatcherInterface.php#L58) - NOT
## Interceptor
In an interceptor a `MethodInvocation` object gets passed to the `invoke` method. We can the decorate the targetted instances so that you run computations before or after any methods on the target are invoked.
```php?start_inline
class MyInterceptor implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation)
{
// Before invocation
// ...
// Method invocation
$result = $invocation->proceed();
// After invocation
// ...
return $result;
}
}
```
With the `MethodInvocation` object, you can access the target method's invocation object, method's and parameters.
* [MethodInvocation::proceed](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/Joinpoint.php#L39) - Invoke method
* [MethodInvocation::getMethod](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MethodInvocation.php) - Get method reflection
* [MethodInvocation::getThis](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/Joinpoint.php#L48) - Get object
* [MethodInvocation::getArguments](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/Invocation.php) - Get parameters
Annotations can be obtained using the reflection API.
```php?start_inline
$method = $invocation->getMethod();
$class = $invocation->getMethod()->getDeclaringClass();
```
* `$method->getAnnotations()`
* `$method->getAnnotation($name)`
* `$class->getAnnotations()`
* `$class->getAnnotation($name)`
## Environment Settings
BEAR.Sunday does not have any special environment mode except `prod`.
A Module and the application itself are unaware of the current environment.
There is no way to get the current "mode", this is intentional to keep the code clean.
# DI
Dependency injection is basically providing the objects that an object needs (its dependencies) instead of having it construct them itself.
With dependency injection, objects accept dependencies in their constructors. To construct an object, you first build its dependencies. But to build each dependency, you need its dependencies, and so on. So when you build an object, you really need to build an object graph.
Building object graphs by hand is labour intensive, error prone, and makes testing difficult. Instead, **Dependency Injector** ([Ray.Di](https://github.com/ray-di/Ray.Di)) can build the object graph for you.
| What is object graph ?
| Object-oriented applications contain complex webs of interrelated objects. Objects are linked to each other by one object either owning or containing another object or holding a reference to another object. This web of objects is called an object graph and it is the more abstract structure that can be used in discussing an application's state. - [Wikipedia](http://en.wikipedia.org/wiki/Object_graph)
Ray.Di is the core DI framework used in BEAR.Sunday, which is heavily inspired by Google [Guice](http://code.google.com/p/google-guice/wiki/Motivation?tm=6) DI framework.See more detail at [Ray.Di Manual](https://ray-di.github.io/manuals/1.0/en/index.html).
# AOP
BEAR.Sunday **AOP** enables you to write code that is executed each time a matching method is invoked. It's suited for cross cutting concerns ("aspects"), such as transactions, security and logging. Because interceptors divide a problem into aspects rather than objects, their use is called Aspect Oriented Programming (AOP).
The method interceptor API implemented is a part of a public specification called [AOP Alliance](http://aopalliance.sourceforge.net/).
## Interceptor
[MethodInterceptors](https://github.com/ray-di/Ray.Aop/blob/2.x/src/MethodInterceptor.php) are executed whenever a matching method is invoked.
They have the opportunity to inspect the call: the method, its arguments, and the receiving instance.
They can perform their cross-cutting logic and then delegate to the underlying method.
Finally, they may inspect the return value or the exception and return. Since interceptors may be applied to many methods and will receive many calls, their implementation should be efficient and unintrusive.
```php
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
class MyInterceptor implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation)
{
// Process before method invocation
// ...
// Original method invocation
$result = $invocation->proceed();
// Process after method invocation
// ...
return $result;
}
}
```
## Bindings
"Find" the target class and method with `Matcher` and bind the interceptor to the matching method in [Module](module.html).
```php
$this->bindInterceptor(
$this->matcher->any(), // In any class,
$this->matcher->startsWith('delete'), // Method(s) names that start with "delete",
[Logger::class] // Bind a Logger interceptor
);
$this->bindInterceptor(
$this->matcher->subclassesOf(AdminPage::class), // Of the AdminPage class or a class inherited from it
$this->matcher->annotatedWith(Auth::class), // Annotated method with the @Auth annotation
[AdminAuthentication::class] //Bind the AdminAuthenticationInterceptor
);
```
There are various matchers.
* [Matcher::any](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L16)
* [Matcher::annotatedWith](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L23)
* [Matcher::subclassesOf](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L30)
* [Matcher::startsWith](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L37)
* [Matcher::logicalOr](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L44)
* [Matcher::logicalAnd](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L51)
* [Matcher::logicalNot](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MatcherInterface.php#L58)
```
With the `MethodInvocation` object, you can access the target method's invocation object, method's and parameters.
* [MethodInvocation::proceed](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/Joinpoint.php#L39) - Invoke method
* [MethodInvocation::getMethod](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/MethodInvocation.php) - Get method reflection
* [MethodInvocation::getThis](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/Joinpoint.php#L48) - Get object
* [MethodInvocation::getArguments](https://github.com/ray-di/Ray.Aop/blob/develop-2/src/Invocation.php) - Pet parameters
Annotations can be obtained using the reflection API.
```php
$method = $invocation->getMethod();
$class = $invocation->getMethod()->getDeclaringClass();
```
* `$method->getAnnotations()` // get method annotations
* `$method->getAnnotation($name)`
* `$class->getAnnotations()` // get class annotations
* `$class->getAnnotation($name)`
## Own matcher
You can have your own matcher.
To create `contains` matcher, You need to provide a class which has two methods. One is `matchesClass` for a class match.
The other one is `matchesMethod` method match. Both return the boolean result of match.
```php
use Ray\Aop\AbstractMatcher;
class ContainsMatcher extends AbstractMatcher
{
/**
* {@inheritdoc}
*/
public function matchesClass(\ReflectionClass $class, array $arguments) : bool
{
list($contains) = $arguments;
return (strpos($class->name, $contains) !== false);
}
/**
* {@inheritdoc}
*/
public function matchesMethod(\ReflectionMethod $method, array $arguments) : bool
{
list($contains) = $arguments;
return (strpos($method->name, $contains) !== false);
}
}
```
Module
```php
class AppModule extends AbstractAppModule
{
protected function configure()
{
// ...
$this->bindInterceptor(
$this->matcher->any(), // In any class,
new ContainsMatcher('user'), // When 'user' contained in method name
[UserLogger::class] // Bind UserLogger class
);
}
};
```
# Resource
A BEAR.Sunday application is [RESTful](http://en.wikipedia.org/wiki/Representational_state_transfer) and is made up of a collection of resources connected by links.
## Object as a service
An HTTP method is mapped to a PHP method in the `ResourceObject` class.
It transfers its resource state as a resource representation from stateless request.
([Representational State Transfer)](http://en.wikipedia.org/wiki/REST)
Here are some examples of a resource object:
```php
class Index extends ResourceObject
{
public $code = 200;
public $headers = [];
public function onGet(int $a, int $b): static
{
$this->body = [
'sum' => $a + $b // $_GET['a'] + $_GET['b']
] ;
return $this;
}
}
```
```php
class Todo extends ResourceObject
{
public function onPost(string $id, string $todo): static
{
$this->code = 201; // status code
$this->headers = [ // header
'Location' => '/todo/new_id'
];
return $this;
}
}
```
The PHP resource class has URIs such as `page://self/index` similar to the URI of the web, and conforms to the HTTP method `onGet`,` onPost`, `onPut`,` onPatch`, `onDelete` interface.
$_GET for `onGet` and $_POST for `onPost` are passed to the arguments of the method depending on the variable name, and the methods of `onPut`,` onPatch`, `onDelete` are content. The value that can be handled according to `content-type`(`x-www-form-urlencoded` or `application/json`) is an argument.
The resource state (`code`,`headers` or`body`) is handled by these method using the given parameters. Then the resource class returns itself(`$this`).
## URI
URIs are mapped to PHP classes. Applications use the URI instead of the class name to access resources.
| URI | Class |
|-----+-------|
| page://self/ | Koriym\Todo\Resource\Page\Index |
| page://self/index | Koriym\Todo\Resource\Page\Index |
| app://self/blog/posts?id=3 | Koriym\Todo\Resource\App\Blog\Posts |
## Scheme
The equivalent to a MVC model is an `app` resource. A resource functions as an internal API, but as it is designed using REST it also works as an external API transport.
The `page` resource carries out a similar role as a page controller which is also a resource. Unlike `app` resources, it receives external requests and generates representations for output.
| URI | Class |
|-----+-------|
| page://self/index | Koriym\Todo\Resource\Page\Index |
| app://self/blog/posts | Koriym\Todo\Resource\App\Blog\Posts |
## Method
Resources have 6 interfaces conforming to HTTP methods.[^method]
[^method]: REST methods are not a mapping to CRUD. They are divided into two categories: safe ones that do not change the resource state, or idempotent ones.
### GET
Reads resources. This method does not provide any changing of the resource state. A safe method with no possible side affects.
### POST
The POST method requests processing of the representation contained in the request. For example, adding a new resource to a target URI or adding a representation to an existing resource. Unlike PUT, requests do not have [idempotence](https://ja.wikipedia.org/wiki/%E5%86%AA%E7%AD%89), and multiple consecutive executions will not produce the same result.
### PUT
Replaces the resource with the payload of the request at the requested URI. If the target resource does not exist, it is created. Unlike POST, PUT is idempotent.
### PATCH
Performs resource updates, but unlike PUT, it applies a delta rather than replacing the entire resource.
### DELETE
Resource deletion. Has idempotence just like PUT.
### OPTIONS
Get information on parameters and responses required for resource request. It is as secure as GET method.
#### List of method properties
| Methods | [Safe](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) | [Idempotent](https://developer.mozilla.org/en-US/docs/Glossary/Idempotent) | [Cacheable](https://developer.mozilla.org/en-US/docs/Glossary/cacheable)
|-|-|-|-|-
| GET | Yes | Yes | Yes
| POST | No | No | No
| PUT | No | Yes | No
| PATCH | No | No | No
| DELETE | No | Yes | No
| OPTIONS | Yes | Yes | No
## Parameters
The response method argument is passed the request value corresponding to the variable name.
```php
class Index extends ResourceObject
{
// $_GET['id'] to $id
public function onGet(int $id): static
{
}
// $_POST['name'] to $name
public function onPost(string $name): static
{
}
```
See [Resource Parameters](resource_param.html) for other methods and how to pass external variables such as cookies as parameters.
## Rendering and transfer
The request method of a ResourceObject is not concerned with the representation of the resource. The injected renderer generates the representation of the resource and the responder outputs it. See [Rendering and Transferring](resource_renderer.html) for details.
## Client
Use the resource client to request other resources. This request executes a request to the `app://self/blog/posts` resource with the query `?id=1`.
```php
use BEAR\Sunday\Inject\ResourceInject;
class Index extends ResourceObject
{
use ResourceInject;
public function onGet(): static
{
$this->body = [
'posts' => $this->resource->get('app://self/blog/posts', ['id' => 1])
];
}
}
```
Other historical notations include the following
```php
// PHP 5.x and up
$posts = $this->resource->get->uri('app://self/posts')->withQuery(['id' => 1])->eager->request();
// PHP 7.x and up
$posts = $this->resource->get->uri('app://self/posts')(['id' => 1]);
// you can omit `get`
$posts = $this->resource->uri('app://self/posts')(['id' => 1]);
// bear/resource 1.11 and up
$posts = $this->resource->get('app://self/posts', ['id' => 1]);
```
## Lazy evaluation
The above is an `eager` request that makes the request immediately, but it is also possible to generate a request and delay execution instead of the request result.
```php
$request = $this->resource->get->uri('app://self/posts')->withQuery(['id' => 1]); // callable
$posts = $request();
```
When this request is embedded in a template or resource, it is evaluated lazily. That is, when it is not evaluated, the request is not made and has no execution cost.
```php
$this->body = [
'lazy' => $this->resource->get->uri('app://self/posts')->withQuery(['id' => 3])
];
```
The `#[Embed]` attribute provides a declarative way to achieve the same lazy embedding. See [Resource Link](resource_link.html) for details.
## Cache
Along with regular TTL caching, we support REST client caching and advanced partial caching (doughnut caching), including CDN. See [cache](cache.html) for details. Also see the previous [resource(v1)](resourcev1.html#Resource Cache) document for the previous `@Cacheable` annotation.
## Link
One important REST constraint is resource linking; ResourceObject supports both internal and external linking. See [Resource Linking](resource_link.html) for details.
## BEAR.Resource
The functionality of the BEAR.Sunday resource object is also available in a stand-alone package for stand-alone use: BEAR.Resource [README](https://github.com/bearsunday/BEAR.Resource/blob/1.x/README.ja.md).
---
## Development
# Database
The following modules are available for database use, with different problem solving methods. They are all independent libraries for SQL based on [PDO](https://www.php.net/manual/ja/intro.pdo.php).
* ExtendedPdo with PDO extended ([Aura.sql](https://github.com/auraphp/Aura.Sql))
* Query Builder ([Aura.SqlQuery](https://github.com/auraphp/Aura.SqlQuery))
* Binding PHP interface and SQL execution ([Ray.MediaQuery](database_media.html))
Having static SQL in a file[^locator] makes it easier to use and tune with other SQL tools. SqlQuery can dynamically assemble queries, but the rest of the library is for basic static SQL execution. Ray.MediaQuery can also replace parts of the SQL with those assembled by the builder.
[^locator]: [query-locater](https://github.com/koriym/Koriym.QueryLocator) is a library for handling SQL as files, which is useful with Aura.Sql.
## Module
Modules are provided for using the database. They are all independent libraries for SQL.
* [Ray.AuraSqlModule](database_aura.html)
* [Ray.MediaQuery](database_media.html)
`Ray.AuraSqlModule` is a PDO extension [Aura.Sql](https://github.com/auraphp/Aura.Sql) and a query builder [Aura.SqlQuery](https://github.com/auraphp/) SqlQuery, plus a low-level module that provides pagination functionality.
`Ray.MediaQuery` is a high-performance DB access framework that generates and injects SQL execution objects from user-provided interfaces and SQL [^doma] .
[^doma]: The mechanism is similar to Java's DB access framework [Doma](https://doma.readthedocs.io/en/latest/basic/#examples).
## Other
* [DBAL](database_dbal.html)
* [CakeDb](database_cake.html)
* [Ray.QueryModule](https://github.com/ray-di/Ray.QueryModule/blob/1.x/README.md)
`DBAL` is Doctrine and `CakeDB` is CakePHP's DB library. `Ray.QueryModule` is an earlier library of Ray.MediaQuery that converts SQL to anonymous functions.
## CQRS Read Model
* [BEAR.Projection](https://github.com/bearsunday/BEAR.Projection)
`BEAR.Projection` provides SQL-based projections mapped to typed value objects. Projections are exposed as resources via the `query://` scheme and can be embedded with `#[Embed]` for [parallel execution](async.html).
```php
#[Embed(rel: 'profile', src: 'query://self/user_profile{?id}')]
#[Embed(rel: 'orders', src: 'query://self/user_orders{?id}')]
public function onGet(string $id): static
```
----
# Router
The router converts resource requests for external contexts such as Web and console into resource requests inside BEAR.Sunday.
```php?start_inline
$request = $app->router->match($GLOBALS, $_SERVER);
echo (string) $request;
// get page://self/user?name=bear
```
## Web Router
The default web router accesses the resource class corresponding to the HTTP request path (`$_SERVER['REQUEST_URI']`).
For example, a request of `/index` is accessed by a PHP method corresponding to the HTTP method of the `{Vendor name}\{Project name}\Resource\Page\Index` class.
The Web Router is a convention-based router. No configuration or scripting is required.
```php?start_inline
namespace MyVendor\MyProject\Resource\Page;
// page://self/index
class Index extends ResourceObject
{
public function onGet(): static // GET request
{
}
}
```
## CLI Router
In the `cli` context, the argument from the console is "input of external context".
```bash
php bin/page.php get /
```
The BEAR.Sunday application works on both the Web and the CLI.
## Multiple words URI
The path of the URI using hyphens and using multiple words uses the class name of Camel Case.
For example `/wild-animal` requests are accessed to the `WildAnimal` class.
## Parameters
The name of the PHP method executed corresponding to the HTTP method and the value passed are as follows.
| HTTP method | PHP method | Parameters |
|---|---|---|
| GET | onGet | $_GET |
| POST | onPost | $_POST or ※ standard input |
| PUT | onPut | ※ standard input |
| PATCH | onPatch | ※ standard input |
| DELETE | onDelete | ※ standard input |
There are two media types available for request:
* `application/x-www-form-urlencoded` // param1=one¶m2=two
* `application/json` // {"param1": "one", "param2": "one"}
Please also see the [PUT method support](http://php.net/manual/en/features.file-upload.put-method.php) of the PHP manual.
## Method Override
There are firewalls that do not allow HTTP PUT traffic or HTTP DELETE traffic.
To deal with this constraint, you can send these requests in the following two ways.
* `X-HTTP-Method-Override` Send a PUT request or DELETE request using the header field of the POST request.
* `_method` Use the URI parameter. ex) POST /users?...&_method=PUT
## Aura Router
To receive the request path as a parameter, use Aura Router.
```bash
composer require bear/aura-router-module ^2.0
```
Install `AuraRouterModule` with the path of the router script.
```php?start_inline
use BEAR\Package\AbstractAppModule;
use BEAR\Package\Provide\Router\AuraRouterModule;
class AppModule extends AbstractAppModule
{
protected function configure()
{
// ...
$this->install(new AuraRouterModule($appDir . '/var/conf/aura.route.php'));
}
}
```
Delete cached DI files to activate new router.
```
rm -rf var/tmp/*
```
### Router Script
Router scripts set routes for `Map` objects passed globally.
You do not need to specify a method for routing.
The first argument specifies the path as the root name and the second argument specifies the path containing the place folder of the named token.
`var/conf/aura.route.php`
```php
route('/blog', '/blog/{id}');
$map->route('/user', '/user/{name}')->tokens(['name' => '[a-z]+']);
$map->route('/blog/comment', '/blog/{id}/comment');
```
* In the first line, accessing `/blog/bear` will be accessed as `page://self/blog?id=bear`.
(= `Blog` class's` onGet($id)` method with the value `$id`=`bear`.)
* `token` is used to restrict parameters with regular expressions.
* `/blog/{id}/comment` to route `Blog\Comment` class.
### Preferred router
If it is not routed by the Aura router, a web router will be used.
In other words, it is OK to prepare the router script only for the URI that passes the parameters in the path.
### Parameter
`Aura router` have various methods to obtain parameters from the path.
### Custom Placeholder Token Matching
The script below routes only when `{date}` is in the proper format.
```php?start_inline
$map->route('/calendar/from', '/calendar/from/{date}')
->tokens([
'date' => function ($date, $route, $request) {
try {
new \DateTime($date);
return true;
} catch(\Exception $e) {
return false;
}
}
]);
```
### Optional Placeholder Tokens
Sometimes it is useful to have a route with optional placeholder tokens for attributes. None, some, or all of the optional values may be present, and the route will still match.
To specify optional attributes, use the notation {/attribute1,attribute2,attribute3} in the path. For example:
ex)
```php?start_inline
$map->route('archive', '/archive{/year,month,day}')
->tokens([
'year' => '\d{4}',
'month' => '\d{2}',
'day' => '\d{2}',
]);
```
Please note that there is the first slash **inside** of the place holder.
Then all the paths below are routed to 'archive' and the value of the parameter is appended.
- `/archive : ['year' => null, 'month' => null, 'day' => null]`
- `/archive/1979 : ['year' => '1979', 'month' => null, 'day' => null]`
- `/archive/1979/11 : ['year' => '1979', 'month' => '11', 'day' => null]`
- `/archive/1979/11/07 : ['year' => '1979', 'month' => '11', 'day' => '07']`
Optional parameters are **options in the order of**. In other words, you can not specify "day" without "month".
### Wildcard Attributes
Sometimes it is useful to allow the trailing part of the path be anything at all. To allow arbitrary trailing path segments on a route, call the wildcard() method. This will let you specify the attribute name under which the arbitrary trailing values will be stored.
```php?start_inline
$map->route('wild', '/wild')
->wildcard('card');
```
All slash-separated path segments after the {id} will be captured as an array in the in wildcard attribute. For example:
- `/wild : ['card' => []]`
- `/wild/foo : ['card' => ['foo']]`
- `/wild/foo/bar : ['card' => ['foo', 'bar']]`
- `/wild/foo/bar/baz : ['card' => ['foo', 'bar', 'baz']]`
For other advanced routes, please refer to Aura Router's [defining-routes](https://github.com/auraphp/Aura.Router/blob/3.x/docs/defining-routes.md).
## Generating Paths From Routes
You can generate a URI from the name of the route and the value of the parameter.
```php?start_inline
use BEAR\Sunday\Extension\Router\RouterInterface;
class Index extends ResourceObject
{
/**
* @var RouterInterface
*/
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onGet(): static
{
$userLink = $this->router->generate('/user', ['name' => 'bear']);
// '/user/bear'
```
### Request Method
It is not necessary to specify a request method.
### Request Header
Normally request headers are not passed to Aura.Router, but installing `RequestHeaderModule` allows Aura.Router to match using headers.
```php
$this->install(new RequestHeaderModule());
```
## Custom Router Component
Implement [RouterInterface](https://github.com/bearsunday/BEAR.Sunday/blob/1.x/src/Extension/Router/RouterInterface.php) with by referring to [BEAR.AuraRouterModule](https://github.com/bearsunday/BEAR.AuraRouterModule).
---
*[This document](https://github.com/bearsunday/bearsunday.github.io/blob/master/manuals/1.0/en/router.md) needs to be proofread by native speaker. *
# Production
For BEAR.Sunday's default `prod` binding, the application customizes the module according to each [deployment environment](https://en.wikipedia.org/wiki/Deployment_environment) and performs the binding.
## Default ProdModule
The default `prod` binding binds the following interfaces:
* Error page generation factory
* PSR logger interface
* Local cache
* Distributed cache
See [ProdModule.php](https://github.com/bearsunday/BEAR.Package/blob/1.x/src/Context/ProdModule.php) in BEAR.Package for details.
## Application's ProdModule
Customize the application's `ProdModule` in `src/Module/ProdModule.php` against the default ProdModule. Error pages and distributed caches are particularly important.
```php
install(new PackageProdModule); // Default prod settings
$this->override(new OptionsMethodModule); // Enable OPTIONS method in production as well
$this->install(new CacheVersionModule('1')); // Specify resource cache version
// Custom error page
$this->bind(ErrorPageFactoryInterface::class)->to(MyErrorPageFactory::class);
}
}
```
## Cache
There are two types of caches: a local cache and a distributed cache that is shared between multiple web servers.
Both caches default to [PhpFileCache](https://www.doctrine-project.org/projects/doctrine-cache/en/1.10/index.html#phpfilecache).
### Local Cache
The local cache is used for caches that do not change after deployment, such as annotations, while the distributed cache is used to store resource states.
### Distributed Cache
To provide services with two or more web servers, a distributed cache configuration is required.
Modules for each of the popular [memcached](http://php.net/manual/en/book.memcached.php) and [Redis](https://redis.io) cache engines are provided.
### Memcached
```php
install(new StorageMemcachedModule($memcachedServers));
// Install Prod logger
$this->install(new ProdLoggerModule);
// Install default ProdModule
$this->install(new PackageProdModule);
}
}
```
### Redis
```php?start_inline
// redis
$redisServer = 'localhost:6379'; // {host}:{port}
$this->install(new StorageRedisModule($redisServer));
```
In addition to simply updating the cache by TTL for storing resource states, it is also possible to operate (CQRS) as a persistent storage that does not disappear after the TTL time.
In that case, you need to perform persistent processing with `Redis` or prepare your own storage adapter for other KVS such as Cassandra.
### Specifying Cache Time
To change the default TTL, install `StorageExpiryModule`.
```php?start_inline
// Cache time
$short = 60;
$medium = 3600;
$long = 24 * 3600;
$this->install(new StorageExpiryModule($short, $medium, $long));
```
### Specifying Cache Version
Change the cache version when the resource schema changes and compatibility is lost. This is especially important for CQRS operation that does not disappear over TTL time.
```
$this->install(new CacheVersionModule($cacheVersion));
```
To discard the resource cache every time you deploy, it is convenient to assign a time or random value to `$cacheVersion` so that no change is required.
## Logging
`ProdLoggerModule` is a resource execution log module for production. When installed, it logs requests other than GET to the logger bound to `Psr\Log\LoggerInterface`.
If you want to log on a specific resource or specific state, bind a custom log to [BEAR\Resource\LoggerInterface](https://github.com/bearsunday/BEAR.Resource/blob/1.x/src/LoggerInterface.php).
```php
use BEAR\Resource\LoggerInterface;
use Ray\Di\AbstractModule;
final class MyProdLoggerModule extends AbstractModule
{
protected function configure(): void
{
$this->bind(LoggerInterface::class)->to(MyProdLogger::class);
}
}
```
The `__invoke` method of [LoggerInterface](https://github.com/bearsunday/BEAR.Resource/blob/1.x/src/LoggerInterface.php) passes the resource URI and resource state as a `ResourceObject` object, so log the necessary parts based on its contents.
Refer to the [existing implementation ProdLogger](https://github.com/bearsunday/BEAR.Resource/blob/1.x/src/ProdLogger.php) for creation.
## Deployment
### ⚠️ Avoid Overwriting Updates
#### When deploying to a server
* Overwriting a running project folder with `rsync` or similar poses a risk of inconsistency with caches and on-demand generated files, and can exceed capacity on high-load sites.
Set up in a separate directory for safety and switch if the setup is successful.
* You can use the [BEAR.Sunday recipe](https://github.com/bearsunday/deploy) of [Deployer](http://deployer.org/).
#### When deploying to the cloud
* It is recommended to incorporate compilation into CI as the compiler outputs exit code 1 when it finds dependency issues and 0 when compilation succeeds.
### Compilation Recommended {: #compilation }
When setting up, you can **warm up** the project: create static cache files for DI/AOP and annotations in advance, and write optimized `autoload.php` and `preload.php`.
**Principle: compile on the deploy target when you can** (at warm-up / health-check time). Real environment values — paths, environment variables — are reflected as-is, so values bake in correctly and no writable-path override is needed.
**Exception: environments that need ahead-of-time compilation** (serverless, a read-only app root, or a fixed path such as `/tmp`). Because you compile where the real services are unreachable, combine [changing writable paths](#writable-paths), AOT script reuse, and `.compile.php` (to stub the unreachable services), and **do not bake values that vary at runtime (hosts, tokens) — resolve them at runtime.**
A build script names the application; it does not boot it (BEAR.Package 1.22+; the skeleton ships `bin/compile.php`).
```php
getInstance(AppInterface::class);
+ $app = Injector::getInstance($context, $writeDir)->getInstance(AppInterface::class);
// src/Injector.php
-use BEAR\Package\Injector\PackageInjector;
+use BEAR\Package\Injector as PackageInjector;
- public static function getInstance(
- string $context, string|null $tmpDir = null, string|null $logDir = null
- ): InjectorInterface
- {
- $meta = new Meta(__NAMESPACE__, $context, dirname(__DIR__), $tmpDir, $logDir);
- $cacheNamespace = str_replace('/', '_', $meta->appDir) . $context;
- $cache = (new LocalCacheProvider($meta->tmpDir . '/injector', $cacheNamespace))->get();
-
- return PackageInjector::getInstance($meta, $context, $cache);
- }
+ public static function getInstance(string $context, string|null $writeDir = null): InjectorInterface
+ {
+ return PackageInjector::getInstance(__NAMESPACE__, $context, dirname(__DIR__), null, $writeDir);
+ }
```
`BEAR\Package\Injector` builds the `Meta` and the injector cache pool from the write directory, so the skeleton's own `Meta` / `LocalCacheProvider` lines go away. Development entry points pass nothing and keep the default paths. Reading environment variables is the entry point's business, not the framework's.
The build takes the directory as an argument, the runtime as an environment variable:
```text
build php bin/compile.php prod-app /tmp
runtime APP_WRITE_DIR=/tmp
php-fpm env[APP_WRITE_DIR] = /tmp
docker --env APP_WRITE_DIR=/tmp
```
With `/tmp` as the write directory, the layout is:
```text
{appDir}/var/tmp/{context}/di compiled DI scripts, in the artifact
/tmp/MyVendor/MyProject/{context}/tmp query repository cache, serialized injector
/tmp/MyVendor/MyProject/{context}/log
```
The application and the context are in the path because local cache keys are resource URIs: two applications or two contexts sharing one directory would answer with each other's entries.
Compiled DI scripts stay under `appDir` and ship inside the artifact. A new instance starts with an empty `/tmp`, so following the write directory would compile again on every cold start - 0.38s against 0.018s on a five-resource application.
If the boot is given a different write directory than the build used, the DI scripts are compiled again instead of read from the old paths: the compile fails if the artifact is read-only, and emits a `Compiled DI scripts on demand` notice if it is writable.
Requires BEAR.Package 1.22+. Background: [BEAR.Package#491](https://github.com/bearsunday/BEAR.Package/pull/491).
### autoload.php
An optimized autoload file is output to `{project_path}/autoload.php`. It is lighter than the `vendor/autoload.php` produced by `composer dump-autoload --optimize` and reduces per-request autoload cost **when you do not use preload**.
Note: If you use `preload.php`, most classes are already loaded at startup, so this `autoload.php` is essentially unnecessary — use the `vendor/autoload.php` generated by Composer. In other words, `autoload.php` is a fallback for environments that cannot use preload.
### preload.php
An optimized preload.php file is output to `{project_path}/preload.php`.
To enable preloading, you need to specify [opcache.preload](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload) and [opcache.preload](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.preload-user) in php.ini. It is a feature supported in PHP 7.4, but it is unstable in the initial versions of `7.4`. Let's use the latest version of `7.4.4` or higher.
Example)
```
opcache.preload=/path/to/project/preload.php
opcache.preload_user=www-data
```
Note: Please refer to the [benchmark](https://github.com/bearsunday/BEAR.HelloworldBenchmark/wiki/Intel-Core-i5-3.8-GHz-iMac-(Retina-5K,-27-inch,-2017)---PHP-7.4.4) for performance benchmarks.
### .compile.php
When there are classes that cannot be generated in a non-production environment (for example, a ResourceObject that requires successful authentication to complete injection), you can compile them by describing dummy class loading in the root `.compile.php` file, which is only loaded during compilation. **Its purpose is to let construction succeed at compile time, so its contents should be null objects (do-nothing implementations).** This applies not only to ahead-of-time builds (real services unreachable) but also to resources that need per-request state such as authentication, which is absent during compilation even when you compile on the deploy target. Keep value fakes (`$_SERVER['X'] = 'fake'`, etc.) to the minimum needed to pass construction, and never use them for values that must be real at runtime (they get baked in).
**Note (BEAR.Package 1.21+):** `Compiler::fromInjector()` does not load `.compile.php` automatically (the deprecated `bear.compile` did). Load it from your `bin/compile.php` as shown above.
.compile.php
Example) If there is an AuthProvider that throws an exception when authentication cannot be obtained in the constructor, you can create an empty class as follows and load it in .compile.php:
/tests/Null/AuthProvider.php
```php
module.svg
```
## Bootstrap Performance Tuning
[immutable_cache](https://pecl.php.net/package/immutable_cache) is a PECL package for caching immutable values in shared memory. It is based on APCu but is faster than APCu because it stores immutable values such as PHP objects and arrays in shared memory. Additionally, installing PECL's [Igbinary](https://www.php.net/manual/en/book.igbinary.php) with either APCu or immutable_cache can reduce memory usage and further improve performance.
Currently, there are no dedicated cache adapters available. Please refer to [ImmutableBootstrap](https://github.com/koriym/BEAR.Hello/commit/507d1ee3ed514686be2d786cdaae1ba8bed63cc4) to create and call a dedicated Bootstrap. This allows you to minimize initialization costs and achieve maximum performance.
### php.ini
```
// Extensions
extension="apcu.so"
extension="immutable_cache.so"
extension="igbinary.so"
// Specifying serializer
apc.serializer=igbinary
immutable_cache.serializer=igbinary
```
`````
----
# Test
Proper testing makes software better with continuity. A clean application of BEAR.Sunday is test friendly, with all dependencies injected and crosscutting interests provided in the AOP.
## Run test
Run `vendor/bin/phpunit` or `composer test`. Other commands are as follows.
```
composer test // phpunit test
composer tests // test + sa + cs
composer coverage // test coverage
composer pcov // test coverage (pcov)
composer sa // static analysis
composer cs // coding standards check
composer cs-fix // coding standards fix
```
## Resource test
**Everything is a resource** - BEAR.Sunday application can be tested with resoure access.
This is a test that tests that `201 (Created)` will be returned by POSTing `['title' => 'test']` to URI `page://self/todo` of `Myvendor\MyProject` application in `html-app` context.
```php
resource = $injector->getInstance(ResourceInterface::class);
}
public function testOnPost(): void
{
$page = $this->resource->post('page://self/todo', ['title' => 'test']);
$this->assertSame(StatusCode::CREATED, $page->code);
}
}
```
## Test Double
A Test Double is a substitute that replaces a component on which the software test object depends. Test doubles can have the following patterns
* Stub (provides "indirect input" to the test target)
* Mock ( validate "indirect output" from the test target inside a test double)
* Spy (records "indirect output" from the target to be tested)
* Fake (simpler implementation that works closer to the actual object)
* _Dummy_ (necessary to generate the test target but no call is made)
### Test Double Binding
There are two ways to change the bundling for a test. One is to change the bundling across all tests in the context module, and the other is to temporarily change the bundling only for a specific purpose within one test only.
#### Context Module
Create a ``TestModule`` to make the `test` context available in bootstrap.
```php
class TestModule extends AbstractModule
{
public function configure(): void
{
$this->bind(DateTimeInterface::class)->toInstance(new DateTimeImmutable('1970-01-01 00:00:00'));
$this->bind(Auth::class)->to(FakeAuth::class);
}
}
```
Injector with test context.
```php
$injector = Injector::getInstance('test-hal-app', $module);
```
#### Temporary binding change
Temporary bundle changes for a single test specify the bundle to override with `Injector::getOverrideInstance`.
```php
public function testBindFake(): void
{
$module = new class extends AbstractModule {
protected function configure(): void
{
$this->bind(FooInterface::class)->to(FakeFoo::class);
}
}
$injector = Injector::getOverrideInstance('hal-app', $module);
}
```
### Mock
```php
public function testBindMock(): void
{
$mock = $this->createMock(FooInterface::class);
// expect that update() will be called once and the parameter will be the string 'something'.
mock->expects($this->once())
->method('update')
->with($this->equalTo('something'));
$module = new class($mock) extends AbstractModule {
public function __construct(
private FooInterface $foo
){}
protected function configure(): void
{
$this->bind(FooInterface::class)->toInstance($this->foo);
}
};
$injector = Injector::getOverrideInstance('hal-app', $module);
}
```
### spy
Installs a `SpyModule` by specifying the interface or class name of the spy target. [^spy-module] After running the SUT containing the spy target, verify the number of calls and the value of the calls in the spy log.
[^spy-module]: [ray/test-double](https://github.com/ray-di/Ray.TestDouble) must be installed to use SpyModule.
```php
public function testBindSpy(): void
{
$module = new class extends AbstractModule {
protected function configure(): void
{
$this->install(new SpyModule([FooInterface::class]));
}
};
$injector = Injector::getOverrideInstance('hal-app', $module);
$resource = $injector->getInstance(ResourceInterface::class);
// Spy logs of FooInterface objects are logged, whether directly or indirectly.
$resource->get('/');
// Spyログの取り出し
$spyLog = $injector->getInstance(\Ray\TestDouble\LoggerInterface::class);
// @var array
* Halo Home (Border and Tools Display)
* Resource State
* Resource Representation
* Profile
You can try a demo of Halo in the [demo](/docs/demo/halo/).
### Performance Monitoring
Halo also displays performance information about the resource, including execution time, memory usage, and a link to the profiler.
### Installation
To enable profiling, you need to install [xhprof](https://www.php.net/manual/en/intro.xhprof.php), which helps identify performance bottlenecks.
```
pecl install xhprof
// Also add 'extension=xhprof.so' to your php.ini file
```
To visualize and graphically display call graphs, you need to install [graphviz](https://graphviz.org/download/).
Example: [Call Graph Demo](/docs/demo/halo/callgraph.svg)
```
// macOS
brew install graphviz
// Windows
// Download and install the installer from the graphviz website
// Linux (Ubuntu)
sudo apt-get install graphviz
```
In your application, create a Dev context module and install the `HaloModule`.
```php
class DevModule extends AbstractModule
{
protected function configure(): void
{
$this->install(new HaloModule($this));
}
}
```
---
# Form
Ray.WebFormModule provides aspect-oriented web form validation powered by [Aura.Input](https://github.com/auraphp/Aura.Input) and [Ray.Di](https://github.com/ray-di/Ray.Di). Form fields, validation rules, submitted values, and rendering helpers are collected in a single form class so the form is easy to test and change.
## Installation
Install `ray/web-form-module` with Composer.
```bash
composer require ray/web-form-module
```
Install `WebFormModule` in your application module.
```php
use Ray\Di\AbstractModule;
use Ray\WebFormModule\WebFormModule;
class AppModule extends AbstractModule
{
protected function configure()
{
$this->install(new WebFormModule());
}
}
```
The legacy `Ray\WebFormModule\AuraInputModule` class remains available as a thin subclass of `WebFormModule` for backward compatibility. New code should use `WebFormModule`.
## Form Class
A self-initializing form class defines fields and validation rules in `init()`. If the form implements `submit()`, the returned values are used as submitted data. See [Aura.Input self-initializing forms](https://github.com/auraphp/Aura.Input/blob/1.x/README.md#self-initializing-forms) for the underlying form API.
```php
use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\SetAntiCsrfTrait;
class MyForm extends AbstractForm
{
use SetAntiCsrfTrait;
public function init()
{
$this->setField('name', 'text')
->setAttribs([
'id' => 'name'
]);
$this->filter->validate('name')->is('alnum');
$this->filter->useFieldMessage('name', 'Name must be alphanumeric only.');
}
public function submit()
{
return $_POST;
}
public function __toString()
{
$form = $this->form();
$form .= $this->helper->tag('div', ['class' => 'form-group']);
$form .= $this->helper->tag('label', ['for' => 'name']);
$form .= 'Name:';
$form .= $this->helper->tag('/label') . PHP_EOL;
$form .= $this->input('name');
$form .= $this->error('name');
$form .= $this->helper->tag('/div') . PHP_EOL;
$form .= $this->input('submit');
$form .= $this->helper->tag('/form');
return $form;
}
}
```
## Controller
Annotate methods that require form validation with `#[FormValidation]`. The `form` argument names the form property on the controller, and `onFailure` names the method called when validation fails.
```php
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use Ray\WebFormModule\Annotation\FormValidation;
use Ray\WebFormModule\FormInterface;
class MyController
{
/** @var FormInterface */
protected $contactForm;
#[Inject]
public function setForm(#[Named("contact_form")] FormInterface $form)
{
$this->contactForm = $form;
}
#[FormValidation(form: "contactForm", onFailure: "badRequestAction")]
public function createAction()
{
// validation success
// More detail for vnd.error+json can be added with #[VndError].
}
public function badRequestAction()
{
// validation failed
}
}
```
## View
When the form provides string rendering, echoing the form renders the complete HTML.
```php
echo $form;
```
You can also render individual inputs and errors.
```php
echo $form->input('name'); //
echo $form->error('name'); // "Name must be alphanumeric only." or blank.
```
## CSRF Protections
CSRF protection is **opt-in** and can be enabled through either of two independent paths:
- **Per-form**: add `use SetAntiCsrfTrait;` to the form. `AntiCsrfInterface` is injected at construction time, the token field is added in `postConstruct()`, and every `apply()` call verifies the token.
- **Per-action**: annotate the validated method with `#[CsrfProtection]`. `AuraInputInterceptor` then injects `AntiCsrfInterface` into the form before `apply()` runs.
Either path causes `AbstractForm::apply()` to throw `CsrfViolationException` on token mismatch. Without either path, no CSRF check is performed.
```php
use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\Annotation\CsrfProtection;
use Ray\WebFormModule\Annotation\FormValidation;
use Ray\WebFormModule\SetAntiCsrfTrait;
class MyForm extends AbstractForm
{
use SetAntiCsrfTrait;
}
class MyController
{
#[FormValidation(form: "contactForm")]
#[CsrfProtection]
public function createAction()
{
}
}
```
You can provide a custom `AntiCsrf` class. See [Applying CSRF Protections](https://github.com/auraphp/Aura.Input#applying-csrf-protections) in Aura.Input for details.
## Migration From 0.x
Version 1.0 drops Doctrine Annotations in favor of native PHP 8 attributes and tightens type declarations. The most common rewrites are:
| Before (0.x) | After (1.0) |
|--------------------------------------------------------------------|---------------------------------------------------------|
| `@FormValidation(form="f", onFailure="badRequest")` | `#[FormValidation(form: 'f', onFailure: 'badRequest')]` |
| `@FormValidation(form="f", antiCsrf=true)` | `#[FormValidation(form: 'f')]` + `#[CsrfProtection]` |
| `@InputValidation(form="f")` | `#[InputValidation(form: 'f')]` |
| `@VndError(message="...", logref="...")` | `#[VndError(message: '...', logref: '...')]` |
| `new AuraInputInterceptor($injector, $reader)` | `new AuraInputInterceptor($injector)` |
| `public function input($input)` / `public function error($input)` | `input(string $input): string` / `error(string $input): string` |
See [CHANGELOG.md](https://github.com/ray-di/Ray.WebFormModule/blob/1.x/CHANGELOG.md) for the full list of breaking changes.
### Automated Migration With Claude Code
Ray.WebFormModule ships a Claude Code skill at [`.claude/skills/migrate-to-1.0/SKILL.md`](https://github.com/ray-di/Ray.WebFormModule/blob/1.x/.claude/skills/migrate-to-1.0/SKILL.md) that walks an AI assistant through the rewrites above: annotations to attributes, `antiCsrf=true` split into `#[CsrfProtection]`, `Reader` argument removal, and `FormInterface` signature updates. Copy that directory into your consuming project's `.claude/skills/` and invoke it with `/migrate-to-1.0`.
## Validation Exception
`#[InputValidation]` throws `Ray\WebFormModule\Exception\ValidationException` when validation fails. This is useful for API applications where the HTML representation is not used.
```php
use Ray\WebFormModule\Annotation\InputValidation;
class Foo
{
#[InputValidation(form: "form1")]
public function createAction($name)
{
// ...
}
}
```
Installing `Ray\WebFormModule\FormVndErrorModule` makes methods annotated with `#[FormValidation]` throw the same validation exception on failure.
```php
use Ray\Di\AbstractModule;
use Ray\WebFormModule\FormVndErrorModule;
use Ray\WebFormModule\WebFormModule;
class FakeVndErrorModule extends AbstractModule
{
protected function configure()
{
$this->install(new WebFormModule());
$this->override(new FormVndErrorModule());
}
}
```
Echo the caught exception's `error` property to get an [application/vnd.error+json](https://tools.ietf.org/html/rfc6906) representation.
```php
echo $e->error;
//{
// "message": "Validation failed",
// "path": "/path/to/error",
// "validation_messages": {
// "name": [
// "Name must be alphanumeric only."
// ]
// }
//}
```
Add more detail to `vnd.error+json` with the `#[VndError]` attribute.
```php
#[FormValidation(form: "contactForm")]
#[VndError(message: "foo validation failed", logref: "a1000", path: "/path/to/error", href: ["_self" => "/path/to/error", "help" => "/path/to/help"])]
public function createAction()
{
}
```
## Demo
Run the demo application from the Ray.WebFormModule repository.
```bash
php -S docs/demo/1.csrf/web.php
```
# Validation
* You can define resource APIs in the JSON schema.
* You can separate the validation code with `#[Valid]`, `#[OnValidate]` attribute.
* Please see the form for validation by web form.
# JSON Schema
The [JSON Schema](http://json-schema.org/) is the standard for describing and validating JSON objects. `@JsonSchema` and the resource body returned by the method of annotated resource class are validated by JSON schema.
### Install
If you want to validate in all contexts including production, create `AppModule`, if validation is done only during development, create `DevModule` and install within it
```php?start_inline
use BEAR\Resource\Module\JsonSchemaModule; // Add this line
use BEAR\Package\AbstractAppModule;
class AppModule extends AbstractAppModule
{
protected function configure()
{
// ...
$this->install(
new JsonSchemaModule(
$appDir . '/var/json_schema',
$appDir . '/var/json_validate'
)
); // Add this line
}
}
```
Create directories for the JSON schema files
```bash
mkdir var/json_schema
mkdir var/json_validate
```
In the `var/json_schema/`, store the JSON schema file which is the specification of the body of the resource, and the `var/json_validate/` stores the JSON schema file for input validation.
### @JsonSchema annotation
Annotate the method of the resource class by adding `@JsonSchema`, then add the `schema` property by specifying the JSON schema file name, which is `user.json` for this purpose.
### schema
src/Resource/App/User.php
```php?start_inline
use BEAR\Resource\Annotation\JsonSchema; // Add this line
class User extends ResourceObject
{
#[JsonSchema('user.json')]
public function onGet(): static
{
$this->body = [
'firstName' => 'mucha',
'lastName' => 'alfons',
'age' => 12
];
return $this;
}
}
```
We will create a JSON schema named `/var/json_schema/user.json`
```json
{
"type": "object",
"properties": {
"firstName": {
"type": "string",
"maxLength": 30,
"pattern": "[a-z\\d~+-]+"
},
"lastName": {
"type": "string",
"maxLength": 30,
"pattern": "[a-z\\d~+-]+"
}
},
"required": ["firstName", "lastName"]
}
```
### key
If the body has an index key, specify it with the key property of the annotation
```php?start_inline
use BEAR\Resource\Annotation\JsonSchema; // Add this line
class User extends ResourceObject
{
#[JsonSchema(key:'user', schema:'user.json')]
public function onGet()
{
$this->body = [
'user' => [
'firstName' => 'mucha',
'lastName' => 'alfons',
'age' => 12
]
];
return $this;
}
}
```
### params
The `params` property specifies the JSON schema file name for the argument validation
```php?start_inline
use BEAR\Resource\Annotation\JsonSchema; // Add this line
class Todo extends ResourceObject
{
#[JsonSchema(key:'user', schema:'user.json', params:'todo.post.json')]
public function onPost(string $title)
```
We place the JSON schema file
**/var/json_validate/todo.post.json**
```json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "/todo POST request validation",
"properties": {
"title": {
"type": "string",
"minLength": 1,
"maxLength": 40
}
}
```
By constantly verifying in a standardized way instead of proprietary documentation, the specification is **reliable and understandable** to both humans and machines.
### target
To apply schema validation to the representation of the resource object (the rendered result) rather than to the body of the ResourceObject, specify the option `target='view'`.
```php
#[JsonSchema(schema: 'user.json', target: 'view')]
```
### Related Links
* [Example](http://json-schema.org/examples.html)
* [Understanding JSON Schema](https://spacetelescope.github.io/understanding-json-schema/)
* [JSON Schema Generator](https://jsonschema.net/#/editor)
## Exception Handling
When JSON Schema validation fails, BEAR.Resource (1.33.0+) throws a typed exception that tells you whether the problem was in the **request** (client error) or the **response** (server bug):
| Exception | HTTP | Meaning |
|-----------|------|---------|
| `JsonSchemaRequestException` | 400 | Input params failed schema validation |
| `JsonSchemaResponseException` | 500 | Response body failed schema validation |
Both extend `JsonSchemaException`, so you can catch either specifically or together.
### Accessing Structured Errors
`$e->getErrors()` returns a `JsonSchemaErrors` collection:
```php
use BEAR\Resource\Exception\JsonSchemaRequestException;
use BEAR\Resource\Exception\JsonSchemaResponseException;
try {
$response = $resource->post('app://self/user', $params);
} catch (JsonSchemaRequestException $e) {
$errors = $e->getErrors(); // JsonSchemaErrors
$errors->count(); // number of violations
$errors->first(); // first JsonSchemaError or null
// Iterate over each error
foreach ($errors as $error) {
echo $error->property; // e.g. "email"
echo $error->message; // human-readable message
}
// Group by field — useful for building API error responses
$byField = $errors->byProperty(); // array
## Tag-based Cache Invalidation
Content caching has dependency issues. If content A depends on content B, and B depends on C, then when C is updated, not only must C's cache and ETag be updated, but also B's cache and ETag (which depends on C), and A's cache and ETag (which depends on B).
BEAR.Sunday solves this problem by having each resource hold the URI of dependent resources as tags. When a resource embedded with `#[Embed]` is modified, the cache and ETag of all related resources are invalidated, and cache regeneration occurs for the next request.
## Donut Cache
Donut caching is a partial caching technique for cache optimization. It separates content into cacheable and non-cacheable parts and combines them for output.
For example, consider content containing a non-cacheable resource like "`Welcome to $name`". The non-cacheable (do-not-cache) part is combined with other cacheable parts for output.
In this case, since the entire content is dynamic, the whole donut is not cached. Therefore, no ETag is output either.
## Donut Hole Cache
When the donut hole part is cacheable, it can be handled the same way as donut cache. In the example above, a weather forecast resource that changes once per hour is cached and included in the news resource.
In this case, since the donut content as a whole (news) is static, the entire content is also cached and given an ETag. This creates cache dependency. When the donut hole content is updated, the entire cached donut needs to be regenerated.
This dependency resolution happens automatically. To minimize computational resources, the donut part computation is reused. When the hole part (weather resource) is updated, the cache and ETag of the entire content are also automatically updated.
### Recursive Donut
The donut structure is applied recursively. For example, if A contains B and B contains C, when C is modified, A's cache and B's cache are reused except for the modified C part. A's and B's caches and ETags are regenerated, but database access for A and B content retrieval and view rendering are not performed.
The optimized partial cache structure performs content regeneration with minimal cost. Clients don't need to know about the content cache structure.
## Event-Driven Content
Traditionally, CDNs considered content requiring application logic as "dynamic" and therefore not cacheable by CDNs. However, some CDNs like Fastly and Akamai now support immediate or tag-based cache invalidation within seconds, making [this thinking obsolete](https://www.fastly.com/blog/leveraging-your-cdn-cache-uncacheable-content).
BEAR.Sunday dependency resolution works not only server-side but also on shared caches. When AOP detects changes and makes PURGE requests to shared caches, related cache invalidation occurs on shared caches just like server-side.
## Conditional Requests
Content changes are managed by AOP, and content entity tags (ETags) are automatically updated. HTTP conditional requests using ETags not only minimize computational resource usage, but responses returning only `304 Not Modified` also minimize network resource usage.
# Usage
For classes to be cached, use the `#[DonutCache]` attribute for donut cache (when embedded content is not cacheable), and `#[CacheableResponse]` for other cases:
```php
use BEAR\RepositoryModule\Annotation\CacheableResponse;
#[CacheableResponse]
class BlogPosting extends ResourceObject
{
public $headers = [
RequestHeader::CACHE_CONTROL => CacheControl::NO_CACHE
];
#[Embed(rel: "comment", src: "page://self/html/comment")]
public function onGet(int $id = 0): static
{
$this->body['article'] = 'hello world';
return $this;
}
public function onDelete(int $id = 0): static
{
return $this;
}
}
```
### recursive donut
The donut structure will be recursively applied.
For example, if A contains B and B contains C and C is modified, A's cache and B's cache will be reused except for the modified C. A's and B's caches and ETags will be regenerated, but DB access to retrieve A's and B's content and rendering of views will not be done.
The optimized structure of the partial cache performs content regeneration with minimal cost. The client does not need to know about the content cache structure.
## Event-driven content
Traditionally, CDNs have believed that content that requires application logic is "dynamic" and therefore cannot be cached by a CDN. However, some CDNs, such as Fastly and Akamai, allow immediate or tag-based cache invalidation within seconds, [this idea is a thing of the past](https://www.fastly.com/blog/leveraging-your-cdn-cache- uncacheable-content).
Sunday dependency resolution is done not only on the server side, but also on the shared cache; when AOP detects a change and makes a PURGE request to the shared cache, the related cache on the shared cache will be invalidated, just like on the server side.
## Conditional request
Content changes are managed by AOP, and the entity tag (ETag) of the content is automatically updated. conditional requests for HTTP using ETag not only minimize the use of computational resources, but responses that only return `304 Not Modified` also minimize the use of network resources. Conditional HTTP requests using ETag not only minimize the use of computational resources, but also minimize the use of network resources by simply returning `304 Not Modified`.
# Usage
Give the class to be cached the attribute `#[DonutCache]` if it is a donut cache (embedded content is not cacheable) and `#[CacheableResponse]` otherwise.
```php
class Todo extends ResourceObject
{
#[CacheableResponse]
public function onPut(int $id = 0, string $todo): static
{
}
#[RefreshCache]
public function onDelete(int $id = 0): static
{
}
}
```
If you give attributes in either way, all the features introduced in the overview will apply.
Caching is not disabled by time (TTL) by default, assuming event-driven content
Note that with `#[DonutCache]` the whole content will not be cached, but with `#[CacheableResponse]` it will be.
## TTL
TTL is specified with `DonutRepositoryInterface::put()`.
`ttl` is the cache time for non-donut holes, `sMaxAge` is the cache time for CDNs.
```php
use BEAR\RepositoryModule\Annotation\CacheableResponse;
#[CacheableResponse]
class BlogPosting extends ResourceObject
{
public function __construct(private DonutRepositoryInterface $repository)
{}
#[Embed(rel: "comment", src: "page://self/html/comment")]
public function onGet(): static
{
// process ...
$this->repository->put($this, ttl:10, sMaxAge:100);
return $this;
}
}
```
### Default TTL value
For event-driven content, changes to the content must be reflected immediately in the cache, so the default TTL varies depending on the CDN module installed. Therefore, the default TTL will vary depending on the CDN module installed: indefinitely (1 year) if the CDN supports tag-based disabling of caching, or 10 seconds if it does not.
The expected cache reflection time is immediate for Fastly, a few seconds for Akamai, and 10 seconds for others.
To customize it, bind it by implementing `CdnCacheControlHeaderSetterInterface` with reference to `CdnCacheControlHeader`.
## Cache invalidation
Use the methods of `DonutRepositoryInterface` to manually invalidate the cache.
This will invalidate not only the specified cache, but also the cache of the ETag, any other resources it depends on, and the cache of the ETag on the server side and, if possible, on the CDN.
```php
interface DonutRepositoryInterface
{
public function purge(AbstractUri $uri): void;
public function invalidateTags(array $tags): void;
}
```
### Invalidate by URI
```php
// example
$this->repository->purge(new Uri('app://self/blog/comment'));
```
### Disable by tag
```php
$this->repository->invalidateTags(['template_a', 'campaign_b']);
```
### Tag Invalidation in CDN
In order to enable tag-based cache invalidation in CDN, you need to implement and bind `PurgerInterface`.
```php
use BEAR\QueryRepository\PurgerInterface;
interface PurgerInterface
{
public function __invoke(string $tag): void;
}
```
### Specify dependent tags.
Use the `SURROGATE_KEY` header to specify the key for PURGE. Use a space as a separator for multiple strings.
```php
use BEAR\QueryRepository\Header;
class Foo
{
public $headers = [
Header::SURROGATE_KEY => 'template_a campaign_b'
];
```
If the cache is invalidated by `template_a` or `campaign_b` tags, Foo's cache and Foo's ETag will be invalidated both server-side and CDN.
### Resource Dependencies.
Use `UriTagInterface` to convert a URI into a dependency tag string.
```php
public function __construct(private UriTagInterface $uriTag)
{}
```
```php
$this->headers[Header::SURROGATE_KEY] = ($this->uriTag)(new Uri('app://self/foo'));
```
This cache will be invalidated both server-side and CDN when `app://self/foo` is modified.
### Make associative array a resource dependency.
```php
// bodyの内容
[
['id' => '1', 'name' => 'a'],
['id' => '2', 'name' => 'b'],
]
```
If you want to generate a list of dependent URI tags from a `body` associative array like the above, you can specify the URI template with the `fromAssoc()` method.
```php
$this->headers[Header::SURROGATE_KEY] = $this->uriTag->fromAssoc(
uriTemplate: 'app://self/item{?id}',
assoc: $this->body
);
```
In the above case, this cache will be invalidated for both server-side and CDN when `app://self/item?id=1` and `app://self/item?id=2` are changed.
## Configuration
### Redis Marshaller
The Redis cache adapter allows you to configure data compression and serialization methods.
A marshaller handles the serialization of PHP objects and arrays when storing them in Redis, and deserialization when retrieving them.
```php
use BEAR\QueryRepository\StorageRedisDsnModule;
$this->install(
new StorageRedisDsnModule(
dsn: 'redis://localhost:6379',
marshallingOptions: [
'enabled' => true,
'type' => 'deflate', // 'default' or 'deflate'
'use_igbinary' => true // requires ext-igbinary
]
)
);
```
**Marshaller types:**
- `default`: Uses PHP's standard serialization (enabling `use_igbinary` uses a more efficient binary format)
- `deflate`: Compresses data before storing (uses zlib)
Use `deflate` if you want to reduce Redis memory usage. This is a trade-off with CPU usage.
## CDN
If you install a module that supports a specific CDN, vendor-specific headers will be output.
```php
$this->install(new FastlyModule())
$this->install(new AkamaiModule())
```
## Multi-CDN
You can also configure a multi-tier CDN and set the TTL according to the role. For example, in this diagram, a multi-functional CDN is placed upstream, and a conventional CDN is placed downstream. Content invalidation is done for the upstream CDN, and the downstream CDN uses it.
# Response headers
Sunday will automatically do the cache control for the CDN and output the header for the CDN. Client cache control is described in `$header` of ResourceObject depending on the content.
This section is important for security and maintenance purposes.
Make sure to specify the `Cache-Control` in all ResourceObjects.
### Cannot cache
Always specify content that cannot be cached.
```php
ResponseHeader::CACHE_CONTROL => CacheControl::NO_STORE
```
### Conditional requests
Check the server for content changes before using the cache. Server-side content changes will be detected and reflected.
```php
ResponseHeader::CACHE_CONTROL => CacheControl::NO_CACHE
```
### Specify client cache time.
The client is cached on the client. This is the most efficient cache, but server-side content changes will not be reflected at the specified time.
Also, this cache is not used when the browser reloads. The cache is used when a transition is made with the `` tag or when a URL is entered.
```php
ResponseHeader::CACHE_CONTROL => 'max-age=60'
```
If response time is important to you, consider specifying SWR.
```php
ResponseHeader::CACHE_CONTROL => 'max-age=30 stale-while-revalidate=10'
```
In this case, when the max-age of 30 seconds is exceeded, the old cached (stale) response will be returned for up to 10 seconds, as specified in the SWR, until a fresh response is obtained from the origin server. This means that the cache will be updated sometime between 30 and 40 seconds after the last cache update, but every request will be a response from the cache and will be fast.
#### RFC7234 compliant clients
To use the client cache with APIs, use an RFC7234 compliant API client.
* iOS [NSURLCache](https://nshipster.com/nsurlcache/)
* Android [HttpResponseCache](https://developer.android.com/reference/android/net/http/HttpResponseCache)
* PHP [guzzle-cache-middleware](https://github.com/Kevinrob/guzzle-cache-middleware)
* JavaScript(Node) [cacheable-request](https://www.npmjs.com/package/cacheable-request)
* Go [lox/httpcache](https://github.com/lox/httpcache)
* Ruby [faraday-http-cache](https://github.com/plataformatec/faraday-http-cache)
* Python [requests-cache](https://pypi.org/project/requests-cache/)
### private
Specify `private` if you do not want to share the cache with other clients. The cache will be saved only on the client side. In this case, do not specify the cache on the server side.
````php
ResponseHeader::CACHE_CONTROL => 'private, max-age=30'
````
> Even if you use shared cache, you don't need to specify `public` in most cases.
## Cache design
APIs (or content) can be divided into two categories: **Information APIs** (Information APIs) and **Computation APIs** (Computation APIs). The **Computation API** is content that is difficult to reproduce and is truly dynamic, making it unsuitable for caching. The Information API, on the other hand, is an API for content that is essentially static, even if it is read from a DB and processed by PHP.
It analyzes the content in order to apply the appropriate cache.
* Information API or Computation API?
* Dependencies are
* Are the comprehension relationships
* Is the invalidation triggered by an event or TTL?
* Is the event detectable by the application or does it need to be monitored?
* Is the TTL predictable or unpredictable?
Consider making cache design a part of the application design process and make it a specification. It should also contribute to the safety of your project throughout its lifecycle.
### Adaptive TTL
Adaptive TTL is the ability to predict the lifetime of content and correctly tell the client or CDN when it will not be updated by an event during that time. For example, when dealing with a stock API, if it is Friday night, we know that the information will not be updated until the start of trading on Monday. We calculate the number of seconds until that time, specify it as the TTL, and then specify the appropriate TTL when it is time to trade.
The client does not need to request a resource that it knows will not be updated.
## #[Cacheable].
The traditional ##[Cacheable] TTL caching is also supported.
Example: 30 seconds cache on the server side, 30 seconds cache on the client.
The same number of seconds will be cached on the client side since it is specified on the server side.
The same number of seconds will be cached on the client side.
use BEAR\RepositoryModule\Annotation\Cacheable;
#[Cacheable(expirySecond: 30)]]
class CachedResource extends ResourceObject
{
````
Example: Cache the resource on the server and client until the specified expiration date (the date in `$body['expiry_at']`)
```php?start_inline
use BEAR\RepositoryModule\Annotation\Cacheable;
#[Cacheable(expiryAt: 'expiry_at')]]
class CachedResource extends ResourceObject
{
```.
See the [HTTP Cache](https://bearsunday.github.io/manuals/1.0/ja/http-cache.html) page for more information.
## Conclusion
Web content can be of the information (data) type or the computation (process) type. Although the former is essentially static, it is difficult to treat it as completely static content due to the problems of managing content changes and dependencies, so the cache was invalidated by TTL even though no content changes occurred. Sunday's caching framework treats information type content as static as possible, maximizing the power of the cache.
## Terminology
* [条件付きリクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/Conditional_requests)
* [ETag (バージョン識別子)](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/ETag)
* [イベントドリブン型コンテンツ](https://www.fastly.com/blog/rise-event-driven-content-or-how-cache-more-edge)
* [ドーナッツキャッシュ / 部分キャッシュ](https://www.infoq.com/jp/news/2011/12/MvcDonutCaching/)
* [サロゲートキー / タグベースの無効化](https://docs.fastly.com/ja/guides/getting-started-with-surrogate-keys)
* ヘッダー
* [Cache-Control](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control)
* [CDN-Cache-Control](https://blog.cloudflare.com/cdn-cache-control/)
* [Vary](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Vary)
* [Stale-While-Revalidate (SWR)](https://www.infoq.com/jp/news/2020/12/ux-stale-while-revalidate/)
# Stream Response
Normally, resources are rendered by renderers into one string and finally `echo`ed out, but then you cannot output content whose size exceeds the memory limit of PHP. With `StreamRenderer` you can stream HTTP output and you can output large size content while keeping memory consumption low. Stream output can also be used in coexistence with existing renderers.
## Change Transferer and Renderer
Use the [StreamTransferInject](https://github.com/bearsunday/BEAR.Streamer/blob/1.x/src/StreamTransferInject.php) trait on the page to render and respond to the stream output. In the example of this download page, since `$body` is made to be a resource variable of the stream, the injected renderer is ignored and the resource is streamed.
```php?start_inline
use BEAR\Streamer\StreamTransferInject;
class Download extends ResourceObject
{
use StreamTransferInject;
public $headers = [
'Content-Type' => 'image/jpeg',
'Content-Disposition' => 'attachment; filename="image.jpg"'
];
public function onGet(): static
{
$fp = fopen(__DIR__ . '/BEAR.jpg', 'r');
$this->body = $fp;
return $this;
}
}
```
## With Renderers
Stream output can coexist with conventional renderers. Normally, Twig renderers and JSON renderers generate character strings, but when a stream is assigned to a part of it, the whole is output as a stream.
This is an example of assigning a `string` and a `resource` variable to the Twig template and generating a page of inline image.
Template
```twig
Hello, {% raw %}{{ name }}{% endraw %}