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.
## 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 optimized fast startup 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.
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
composer create-project -n bear/skeleton MyVendor.MyProject
cd MyVendor.MyProject
```
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` folder need write permission.
## 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);
```
Bindings declared first take priority
More info can be found at Ray.Di [README](https://github.com/ray-di/Ray.Di/blob/2.x/README.md)
## 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('app://self/posts'); // callable
$posts = $request(['id' => 1]);
```
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('app://self/posts')->withQuery(['id' => 3])->request();
];
```
## 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.
----
# 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
When setting up, you can **warm up** the project using the `vendor/bin/bear.compile` script.
The compile script creates all static cache files such as dynamically created files for DI/AOP and annotations in advance, and outputs an optimized autoload.php file and preload.php.
* If you compile, the possibility of DI errors at runtime is extremely low because injection is performed in all classes.
* The contents included in `.env` are incorporated into the PHP file, so `.env` can be deleted after compilation.
When compiling multiple contexts (ex. api-app, html-app) in one application, such as when performing content negotiation, it is necessary to evacuate the files.
```
mv autoload.php api.autoload.php
```
Edit `composer.json` to change the content of `composer compile`.
### autoload.php
An optimized autoload.php file is output to `{project_path}/autoload.php`.
It is much faster than `vendor/autoload.php` output by `composer dumpa-autoload --optimize`.
Note: If you use `preload.php`, most of the classes used are loaded at startup, so the compiled `autoload.php` is not necessary. Please use `vendor/autload.php` generated by Composer.
### 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.
.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
Each related function of Web Forms using [Aura.Input](https://github.com/auraphp/Aura.Input) and [Aura.Filter](https://github.com/auraphp/Aura.Filter) is aggregated to a single class so that it is easy to test and change.
We can use a corresponding class for the use of Web Forms and validation.
## Install
Install `ray/web-form-module` via composer to add form using Aura.Input
```bash
composer require ray/web-form-module
```
Install `AuraInputModule` in our application module `src/Module/AppModule.php`
```php
use BEAR\Package\AbstractAppModule;
use Ray\WebFormModule\WebFormModule;
class AppModule extends AbstractAppModule
{
protected function configure()
{
// ...
$this->install(new AuraInputModule);
}
}
```
## Web Form
Create **a form class** that defines the registration and the rules of form elements, then bind it to a method using `@FormValidation` annotation.
The method runs only when the sent data is validated.
```php
use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\SetAntiCsrfTrait;
class MyForm extends AbstractForm
{
/**
* {@inheritdoc}
*/
public function init()
{
// set input fields
$this->setField('name', 'text')
->setAttribs([
'id' => 'name'
]);
// set rules and user defined error message
$this->filter->validate('name')->is('alnum');
$this->filter->useFieldMessage('name', 'Name must be alphabetic only.');
}
}
```
We can register the input elements in the `init()` method of the form class and apply the rules of validation and sanitation.
Please refer to [Rules To Validate Fields](https://github.com/auraphp/Aura.Filter/blob/2.x/docs/validate.md) of Aura.Filter with respect to validation rules, and [Rules To Sanitize Fields](https://github.com/auraphp/Aura.Filter/blob/2.x/docs/sanitize.md) with respect to sanitize rules.
We validate an associative array of the argument of the method.
If we want to change the input, we can set the values by implementing `submit()` method of `SubmitInterface` interface.
## @FormValidation Annotation
Annotate the method that we want to validate with the `@FormValidation`, so that the validation is done in the form object specified by the `form` property before execution.
When validation fails, the method with the `ValidationFailed` suffix is called.
```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 $form;
/**
* @Inject
* @Named("contact_form")
*/
public function setForm(FormInterface $form)
{
$this->form = $form;
}
/**
* @FormValidation
* // or
* @FormValidation(form="form", onFailure="onPostValidationFailed")
*/
public function onPost($name, $age)
{
// validation success
}
public function onPostValidationFailed($name, $age)
{
// validation failed
}
}
```
We can explicitly specify the name and the method by changing the `form` property of `@FormValidation` annotation or the `onValidationFailed` property.
The submit parameters will be passed to the `onPostValidationFailed` method.
## View
Specify the element name to get the `input` elements and error messages
```php
$form->input('name'); //
$form->error('name'); // "Please enter a double-byte characters or letters in the name." or blank
```
The same applies to Twig template
```php
{% raw %}{{ form.input('name') }}
{{ form.error('name') }}{% endraw %}
```
## CSRF Protections
We can add a CSRF(Cross site request forgeries) object to the form to apply CSRF protections.
```php
use Ray\WebFormModule\SetAntiCsrfTrait;
class MyForm extends AbstractForm
{
use SetAntiCsrfTrait;
```
In order to increase the security level, add a custom CSRF class that contains the user authentication to the form class.
Please refer to the [Applying CSRF Protections](https://github.com/auraphp/Aura.Input#applying-csrf-protections) of Aura.Input for more information.
## @InputValidation annotation
If we annotate the method with `@InputValidation` instead of `@FormValidation`, the exception `Ray\WebFormModule\Exception\ValidationException` is thrown when validation fails.
For convenience, HTML representation is not used in this case.
When we `echo` the `error` property of the caught exception, we can see the representation of the media type [application/vnd.error+json](https://github.com/blongden/vnd.error).
```php
http_response_code(400);
echo $e->error;
// {
// "message": "Validation failed",
// "path": "/path/to/error",
// "validation_messages": {
// "name": [
// "Please enter a double-byte characters or letters in the name."
// ]
// }
// }
```
We can add the necessary information to `vnd.error+json` using `@VndError` annotation.
```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 onPost()
```
## FormVndErrorModule
If we install `Ray\WebFormModule\FormVndErrorModule`, the method annotated with `@FormValidation`
will throw an exception in the same way as the method annotated with `@InputValidation`.
We can use the page resources as API.
```php
class FooModule extends AbstractModule
{
protected function configure()
{
$this->install(new AuraInputModule);
$this->override(new FormVndErrorModule);
}
}
```
## Demo
Try the demo app [MyVendor.ContactForm](https://github.com/bearsunday/MyVendor.ContactForm) to get an idea on how forms such as
a confirmation form and multiple forms in a single page work.
# Validation
* You can define resource APIs in the JSON schema.
* You can separate the validation code with `@Valid`, `@OnValidate` annotation.
* 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)
## @Valid annotation
The `@Valid` annotation is a validation for input.
You can set up validation as AOP for your method.
By separating validation logic from the method, the code will be readable and testable.
Validation libraries are available such as [Aura.Filter](https://github.com/auraphp/Aura.Filter), [Respect\Validation](https://github.com/Respect/Validation), and [PHP Standard Filter](http://php.net/manual/en/book.filter.php)
### Install
Install `Ray.ValidateModule` via composer.
```bash
composer require ray/validate-module
```
Installing `ValidateModule` in your application module `src/Module/AppModule.php`.
```php?start_inline
use Ray\Validation\ValidateModule;
class AppModule extends AbstractAppModule
{
protected function configure()
{
// ...
$this->install(new ValidateModule);
}
}
```
### Annotation
There are three annotations `@Valid`, `@OnValidate`, `@OnFailure` for validation.
First of all, annotate the method that you want to validate with `@Valid`
```php?start_inline
use Ray\Validation\Annotation\Valid;
class News
{
/**
* @Valid
*/
public function createUser($name)
{
```
Validation will be conducted in the method annotated with `@OnValidate`.
The arguments of the method should be the same as the original method. The method name can be anything.
```php?start_inline
use Ray\Validation\Annotation\OnValidate;
class News
{
/**
* @OnValidate
*/
public function onValidate($name)
{
$validation = new Validation;
if (! is_string($name)) {
$validation->addError('name', 'name should be string');
}
return $validation;
}
```
Add validations to your elements by `addError()` with the `element name` and` error message` as parameters, then return the validation object.
When validation fails, the exception `Ray\Validation\Exception\InvalidArgumentException` will be thrown,
but if you have a method annotated with the `@OnFailure`, it will be called, instead of throwing an exception
```php?start_inline
use Ray\Validation\Annotation\OnFailure;
class News
{
/**
* @OnFailure
*/
public function onFailure(FailureInterface $failure)
{
// original parameters
list($this->defaultName) = $failure->getInvocation()->getArguments();
// errors
foreach ($failure->getMessages() as $name => $messages) {
foreach ($messages as $message) {
echo "Input '{$name}': {$message}" . PHP_EOL;
}
}
}
```
In the method annotated with `@OnFailure`, you can access the validated messages with `$failure->getMessages()`
and also you can get the object of the original method with `$failure->getInvocation()`.
### Various validation
If you want to have different validations for a class, you can specify the name of the validation like below
```php?start_inline
use Ray\Validation\Annotation\Valid;
use Ray\Validation\Annotation\OnValidate;
use Ray\Validation\Annotation\OnFailure;
class News
{
/**
* @Valid("foo")
*/
public function fooAction($name, $address, $zip)
{
/**
* @OnValidate("foo")
*/
public function onValidateFoo($name, $address, $zip)
{
/**
* @OnFailure("foo")
*/
public function onFailureFoo(FailureInterface $failure)
{
```
### Other validation
If you need to implement complex validation, you can have another class for validation and inject it.
And then call in the method annotated with the `onValidate`.
You can also change your validation behavior by context with DI.
# Content Negotiation
In HTTP, [Content Negotiation](https://en.wikipedia.org/wiki/Content_negotiation) is a mechanism used to provide various versions of resources for the same URL. BEAR.Sunday supports server-side content negotiation of media type 'Accept' and 'Accept-Language' of language. It can be specified on an application basis or resource basis.
## Install
Install [BEAR.Accept](https://github.com/bearsunday/BEAR.Accept) with composer
```bash
composer require bear/accept
```
Next, save the context corresponding to the `Accept *` request header in `/var/locale/available.php`
```php?
[
'text/hal+json' => 'hal-app',
'application/json' => 'app',
'cli' => 'cli-hal-app'
],
'Accept-Language' => [ // lower case for key
'ja-jp' => 'ja',
'ja' => 'ja',
'en-us' => 'en',
'en' => 'en'
]
];
```
The `Accept` key array specifies an array whose context is a value with the media type as a key. `cli` is not used in web access in the context of console access.
The `Accept-Language` key array specifies an array with the context key as the key for the language.
## Enable by Application
Change `public/index.php` to enable content negotiation **throughout the application**.
```php
install(new AcceptModule($available));
}
```
## @Produces annotation
```php?start_inline
use BEAR\Accept\Annotation\Produces;
/**
* @Produces({"application/hal+json", "text/csv"})
*/
public function onGet()
```
Annotate available media type from left by priority. The representation (JSON or HTML) is changed by the contextual renderer. You do not need to add `Vary` header manually unlike application level content-negotiation.
## Access using curl
Specify the `Accept*` header with the `-H` option.
```
curl -H 'Accept-Language: en' http://127.0.0.1:8080/
```
```
curl -i -H 'Accept-Language: en' -H 'Accept: application/hal+json' http://127.0.0.1:8080/
```
```
HTTP/1.1 200 OK
Host: 127.0.0.1:8080
Date: Fri, 11 Aug 2017 08:32:33 +0200
Connection: close
X-Powered-By: PHP/7.1.4
Vary: Accept, Accept-Language
content-type: application/hal+json
{
"greeting": "Hello BEAR.Sunday",
"_links": {
"self": {
"href": "/index"
}
}
}
```
# Hypermedia API
## HAL
BEAR.Sunday supports the [HAL](https://en.wikipedia.org/wiki/Hypertext_Application_Language) hypermedia (`application/hal+json`) API.
The HAL resource model consists of the following elements:
* Link
* Embedded resources
* State
HAL is the JSON which represents only the state of the conventional resource plus the link `_links` plus `_embedded` to embed other resources. HAL makes API searchable and can find its API document from the API itself.
### Links
Resources should have a `self` URI
```
{
"_links": {
"self": { "href": "/user" }
}
}
```
### Link Relations
Link rels are the main way of distinguishing between a resource's links.
There is a `rel` (relation) on the link, and it shows how the relationship is linked. It is similar to the `rel` used in the HTML `` and `` tag.
```
{
"_links": {
"next": { "href": "/page=2" }
}
}
```
For more information about HAL please visit [http://stateless.co/hal_specification.html](http://stateless.co/hal_specification.html).
## Resource Class
You can annotate links and embed other resources.
### #[Link]
You can declaratively describe the `@Link` annotation, or dynamic ones are assigned to `body['_links']`.
```php?start_inline
#[Link(rel="user", href="/user")]
#[Link(rel="latest-post", href="/latest-post", title="latest post entrty")]
public function onGet()
```
or
```php?start_inline
public function onGet() {
if ($hasCommentPrivilege) {
$this->body += [
'_links' => [
'comment' => [
'href' => '/comments/{post-id}',
'templated' => true
]
]
];
}
}
```
### #[Embed]
To embed other resources statically, use the `@Embed` annotation, and to embed it dynamically, assign the "request" to` body`.
```php?start_inline
#[Embed(rel="todos", src="/todos{?status}")]
#[Embed(rel="me", src="/me")]
public function onGet(string $status): static
```
or
```php?start_inline
$this->body['_embedded']['todos'] = $this->resource->uri('app://self/todos');
```
## API document service
The API server can also be an API document server. It solves problems such as the time required to create the API document, deviation from actual API, verification, maintenance.
In order for it to be on service, install `bear/api-doc` and install it by inheriting the `BEAR\ApiDoc\ApiDoc` page class
```
composer require bear/api-doc
```
```php
install(new ImportAppModule([
+ new ImportApp('foo', 'MyVendor\Weekday', 'prod-app')
+ ]));
$this->install(new PackageModule());
}
}
```
## Request
The imported resource will be used with the specified host name.
```php
class Index extends ResourceObject
{
use ResourceInject;
public function onGet(string $name = 'BEAR.Sunday'): static
{
$weekday = $this->resource->get('app://foo/weekday?year=2022&month=1&day=1');
$this->body = [
'greeting' => 'Hello ' . $name,
'weekday' => $weekday
];
return $this;
}
}
````
You can also use `#[Embed]` and `#[Link]` in the same way.
## Requests from other systems
It is easy to use BEAR resources from other frameworks or CMS.
Install it as a package in the same way, and use `Injector::getInstance` to get the resource client of the application you require and request it.
```php
use BEAR\Package\Injector;
use BEAR\Resource\ResourceInterface;
$resource = Injector::getInstance(
'MyVendor\Weekday',
'prod-api-app',
dirname(__DIR__) . '/vendor/my-vendor/weekday'
)->getInstance(ResourceInterface::class);
$weekdday = $resource->get('/weekday', ['year' => '2022', 'month' => '1', 'day' => 1]);
echo $weekdday->body['weekday'] . PHP_EOL;
```
## Environment variables
Environment variables are global. Care should be taken to prefix them to avoid conflicts between applications. Instead of using `.env` files, the application to be imported will get the shell environment variables just like in production.
## System Boundary
It is similar to microservices in that a large application can be built as a collection of multiple smaller applications, but without the disadvantages of microservices such as increased infrastructure overhead. It also has clearer component independence and boundaries than modular monoliths.
The code for this page can be found at [bearsunday/example-app-import](https://github.com/bearsunday/example-import-app/commits/master).
## Multilingual Framework
Using [BEAR.Thrift](https://github.com/bearsunday/BEAR.Thrift), you can access resources from other languages, different versions of PHP, or BEAR applications using Apache Thrift. [Apache Thrift](https://thrift.apache.org/) is a framework that enables efficient communication between different languages.
# Cache
> There are only two hard things in Computer Science: cache invalidation and naming things.
>
> -- Phil Karlton
## Overview
A good caching system fundamentally improves the quality of user experience and reduces resource utilization costs and environmental impact. BEAR.Sunday supports the following caching features in addition to traditional simple TTL-based caching:
* Event-driven cache invalidation
* Cache dependency resolution
* Donut cache and donut hole cache
* CDN control
* Conditional requests
### Distributed Cache Framework
A distributed caching system that follows REST constraints saves not only computational resources but also network resources. BEAR.Sunday provides a caching framework that integrates **server-side caches** (such as Redis and APC handled directly by PHP), **shared caches** (known as content delivery networks - CDNs), and **client-side caches** (cached by web browsers and API clients) with modern CDNs.
## 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.
## 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 %}