Tutorial

In this tutorial, we introduce the basic features of BEAR.Sunday, including DI (Dependency Injection), AOP (Aspect-Oriented Programming), and REST API. Follow along with the commits from tutorial1.

Project Creation

Let’s create a web service that returns the day of the week when a date (year, month, day) is entered. Start by creating a project.

composer create-project bear/skeleton MyVendor.Weekday

Enter MyVendor for the vendor name and Weekday for the project name. 1

Resources

First, create an application resource file at src/Resource/App/Weekday.php.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Resource\App;

use BEAR\Resource\ResourceObject;
use DateTimeImmutable;

class Weekday extends ResourceObject
{
    public function onGet(int $year, int $month, int $day): static
    {
        $dateTime = (new DateTimeImmutable)->createFromFormat('Y-m-d', "$year-$month-$day");
        $weekday = $dateTime->format('D');
        $this->body = ['weekday' => $weekday];

        return $this;
    }
}

This resource class MyVendor\Weekday\Resource\App\Weekday can be accessed via the path /weekday. The query parameters of the GET method are passed to the onGet method.

Try accessing it via the console. First, test with an error.

php bin/app.php get /weekday
400 Bad Request
content-type: application/vnd.error+json

{
    "message": "Bad Request",
    "logref": "e29567cd",

Errors are returned in the application/vnd.error+json media type. The 400 error code indicates a problem with the request. Each error is assigned a logref ID, and the details of the error can be found in var/log/.

Next, try a correct request with parameters.

php bin/app.php get '/weekday?year=2001&month=1&day=1'
200 OK
Content-Type: application/hal+json

{
    "weekday": "Mon",
    "_links": {
        "self": {
            "href": "/weekday?year=2001&month=1&day=1"
        }
    }
}

The result is correctly returned in the application/hal+json media type.

Let’s turn this into a Web API service. Start the built-in server.

php -S 127.0.0.1:8080 bin/app.php

Test it with an HTTP GET request using curl.

Modify public/index.php as shown below:

<?php

declare(strict_types=1);

use MyVendor\Weekday\Bootstrap;

require dirname(__DIR__) . '/autoload.php';
- exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'hal-app' : 'prod-hal-app', $GLOBALS, $_SERVER));
+ exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'hal-api-app' : 'prod-hal-api-app', $GLOBALS, $_SERVER));
curl -i 'http://127.0.0.1:8080/weekday?year=2001&month=1&day=1'
HTTP/1.1 200 OK
Host: 127.0.0.1:8080
Date: Tue, 04 May 2021 01:55:59 GMT
Connection: close
X-Powered-By: PHP/8.0.3
Content-Type: application/hal+json

{
    "weekday": "Mon",
    "_links": {
        "self": {
            "href": "/weekday/2001/1/1"
        }
    }
}

This resource class does not have methods other than GET, so trying other methods will return 405 Method Not Allowed. Let’s test this as well.

curl -i -X POST 'http://127.0.0.1:8080/weekday?year=2001&month=1&day=1'
HTTP/1.1 405 Method Not Allowed
...

The HTTP OPTIONS method request can be used to determine the available HTTP methods and required parameters (RFC7231).

curl -i -X OPTIONS http://127.0.0.1:8080/weekday
HTTP/1.1 200 OK
...
Content-Type: application/json
Allow: GET

{
    "GET": {
        "parameters": {
            "year": {
                "type": "integer"
            },
            "month": {
                "type": "integer"
            },
            "day": {
                "type": "integer"
            }
        },
        "required": [
            "year",
            "month",
            "day"
        ]
    }
}

Testing

Let’s create a test for the resource using PHPUnit.

tests/Resource/App/WeekdayTest.php with the following test code:

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Resource\App;

use BEAR\Resource\ResourceInterface;
use MyVendor\Weekday\Injector;
use PHPUnit\Framework\TestCase;

class WeekdayTest extends TestCase
{
    private ResourceInterface $resource;

    protected function setUp(): void
    {
        $injector = Injector::getInstance('app');
        $this->resource = $injector->getInstance(ResourceInterface::class);
    }

    public function testOnGet(): void
    {
        $ro = $this->resource->get('app://self/weekday', ['year' => '2001', 'month' => '1', 'day' => '1']);
        $this->assertSame(200, $ro->code);
        $this->assertSame('Mon', $ro->body['weekday']);
    }
}

The setUp() method specifies the context (app) and uses the application’s injector Injector to obtain a resource client (ResourceInterface), and the testOnGet method requests the resource for testing.

Let’s run it.

./vendor/bin/phpunit
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 00:00.281, Memory: 14.00 MB

The installed project also includes commands for running tests and code inspections. To obtain test coverage, run composer coverage.

composer coverage

pcov can measure coverage more quickly.

composer pcov

You can view the details of the coverage by opening build/coverage/index.html in a web browser.

To check if the coding standards are being followed, use the composer cs command. Automatic corrections can be done with the composer cs-fix command.

composer cs
composer cs-fix

Static Analysis

Static analysis of the code is performed using the composer sa command.

composer sa

When running the code up to this point, the following error was detected by phpstan.

 ------ --------------------------------------------------------- 
  Line   src/Resource/App/Weekday.php                             
 ------ --------------------------------------------------------- 
  15     Cannot call method format() on DateTimeImmutable|false.  
 ------ --------------------------------------------------------- 

The earlier code did not consider that DateTimeImmutable::createFromFormat might return false when invalid values (such as the year being -1) are passed.

Let’s try it.

php bin/app.php get '/weekday?year=-1&month=1&day=1'

Even if a PHP error occurs, the error handler catches it and displays the error message in the correct application/vnd.error+json media type, but to pass static analysis inspection, you need to add code to assert the result of DateTimeImmutable or check the type and throw an exception.

Using assert

$dateTime =(new DateTimeImmutable)->createFromFormat('Y-m-d', "$year-$month-$day");
assert($dateTime instanceof DateTimeImmutable);

Throwing an exception

First, create a dedicated exception src/Exception/InvalidDateTimeException.php.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Exception;

use RuntimeException;

class InvalidDateTimeException extends RuntimeException
{
}

Modify the code to check values.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Resource\App;

use BEAR\Resource\ResourceObject;
use DateTimeImmutable;
+use MyVendor\Weekday\Exception\InvalidDateTimeException;

class Weekday extends ResourceObject
{
    public function onGet(int $year, int $month, int $day): static
    {
        $dateTime = (new DateTimeImmutable)->createFromFormat('Y-m-d', "$year-$month-$day");
+        if (! $dateTime instanceof DateTimeImmutable) {
+            throw new InvalidDateTimeException("$year-$month-$day");
+        }

        $weekday = $dateTime->format('D');
        $this->body = ['weekday' => $weekday];

        return $this;
    }
}

Add a test as well.

+    public function tesInvalidDateTime(): void
+    {
+        $this->expectException(InvalidDateTimeException::class);
+        $this->resource->get('app://self/weekday', ['year' => '-1', 'month' => '1', 'day' => '1']);
+    }

Best Practices for Exception Creation

Since the exception occurred due to an input mistake, there is no problem with the code itself. Such exceptions that become apparent at runtime are RuntimeExceptions. We have extended this to create a dedicated exception. Conversely, if the occurrence of an exception is due to a bug requiring code correction, you would extend LogicException to create the exception. Instead of explaining the type of exception in the message, create dedicated exceptions for each type.

Defensive Programming

This modification eliminates the possibility of false being in $dateTime when executing $dateTime->format('D');. This type of programming, which avoids problems before they occur, is called defensive programming, and static analysis is helpful for these inspections.

Pre-Commit Testing

composer tests not only performs composer test but also checks coding standards (cs) and static analysis (sa).

composer tests

Routing

The default router is WebRouter, which maps URLs to directories. Here, we use the Aura router to accept dynamic parameters in the path.

First, install it with composer.

composer require bear/aura-router-module ^2.0

Next, install AuraRouterModule in src/Module/AppModule.php before PackageModule.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Module;

use BEAR\Dotenv\Dotenv;
use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;
+use BEAR\Package\Provide\Router\AuraRouterModule;
use function dirname;

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        (new Dotenv())->load(dirname(__DIR__, 2));
+        $appDir = $this->appMeta->appDir;
+        $this->install(new AuraRouterModule($appDir . '/var/conf/aura.route.php'));
        $this->install(new PackageModule());
    }
}

Place the router script file in var/conf/aura.route.php.

<?php
/** 
 * @see http://bearsunday.github.io/manuals/1.0/ja/router.html
 * @var \Aura\Router\Map $map 
 */

$map->route('/weekday', '/weekday/{year}/{month}/{day}');

Let’s try it.

php bin/app.php get /weekday/1981/09/08
200 OK
Content-Type: application/hal+json

{
    "weekday": "Tue",
    "_links": {
        "self": {
            "href": "/weekday/1981/09/08"
        }
    }
}

DI

Let’s add a feature to log the requested weekday.

First, create src/MyLoggerInterface.php to log the weekday.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday;

interface MyLoggerInterface
{
    public function log(string $message): void;
}

The resource will be modified to use this logging feature.

<?php
namespace MyVendor\Weekday\Resource\App;

use BEAR\Resource\ResourceObject;
use MyVendor\Weekday\MyLoggerInterface;

class Weekday extends ResourceObject
{
+    public function __construct(public MyLoggerInterface $logger)
+    {
+    }

    public function onGet(int $year, int $month, int $day): static
    {
        $weekday = (new DateTimeImmutable)->createFromFormat('Y-m-d', "$year-$month-$day")->format('D');
        $this->body = [
            'weekday' => $weekday
        ];
+        $this->logger->log("$year-$month-$day {$weekday}");

        return $this;
    }
}

The Weekday class receives the logger service via the constructor. This mechanism, where the necessary objects (dependencies) are not created with new or obtained from a container but are instead injected from outside, is called DI.

Next, implement MyLoggerInterface in MyLogger.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday;

use BEAR\AppMeta\AbstractAppMeta;

use function error_log;

use const PHP_EOL;

class MyLogger implements MyLoggerInterface
{
    private string $logFile;

    public function __construct(AbstractAppMeta $meta)
    {
        $this->logFile = $meta->logDir . '/weekday.log';
    }

    public function log(string $message): void
    {
        error_log($message . PHP_EOL, 3, $this->logFile);
    }
}

Implementing MyLogger requires information about the application’s log directory (AbstractAppMeta), which is also received as a dependency in the constructor. Thus, while the Weekday resource depends on MyLogger, MyLogger also depends on the log directory information. In this way, objects constructed with DI are recursively injected with their dependencies.

This dependency resolution is performed by the DI tool (dependency injector).

To bind MyLoggerInterface and MyLogger using the DI tool, edit the configure method in src/Module/AppModule.php.

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        (new Dotenv())->load(dirname(__DIR__, 2));
        $appDir = $this->appMeta->appDir;
        $this->install(new AuraRouterModule($appDir . '/var/conf/aura.route.php'));
+        $this->bind(MyLoggerInterface::class)->to(MyLogger::class);
        $this->install(new PackageModule());
    }
}

This allows any class to receive a logger via the constructor using MyLoggerInterface.

Run it and check that the results are output to var/log/cli-hal-api-app/weekday.log.

php bin/app.php get /weekday/2011/05/23
cat var/log/cli-hal-api-app/weekday.log

AOP

Let’s consider a benchmarking process to measure the execution time of methods.

$start = microtime(true);
// method invokation
$time = microtime(true) - $start;

Adding this code every time you perform a benchmark and removing it when it’s no longer needed is cumbersome. Aspect-Oriented Programming (AOP) allows you to nicely synthesize such specific pre- and post-method processes.

First, to achieve AOP, create an interceptor that hijacks the method execution and performs the benchmark in src/Interceptor/BenchMarker.php.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Interceptor;

use MyVendor\Weekday\MyLoggerInterface;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;

use function microtime;
use function sprintf;

class BenchMarker implements MethodInterceptor
{
    public function __construct(private MyLoggerInterface $logger)
    {
    }

    public function invoke(MethodInvocation $invocation): mixed
    {
        $start = microtime(true);
        $result = $invocation->proceed(); // Execute the original method
        $time = microtime(true) - $start;
        $message = sprintf('%s: %0.5f(µs)', $invocation->getMethod()->getName(), $time);
        $this->logger->log($message);

        return $result;
    }
}

In the interceptor’s invoke method, the original method’s execution can be performed using $invocation->proceed();, and the timer reset and recording process are performed before and after this. (The original method’s object and method name are obtained from the method execution object MethodInvocation $invocation.)

Next, create an attribute to mark the method you want to benchmark in src/Annotation/BenchMark.php.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Annotation;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
final class BenchMark
{
}

In AppModule, use Matcher to bind (bind) the interceptor to the method to which you want to apply it.

+use MyVendor\Weekday\Annotation\BenchMark;
+use MyVendor\Weekday\Interceptor\BenchMarker;

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        (new Dotenv())->load(dirname(__DIR__, 2));
        $appDir = $this->appMeta->appDir;
        $this->install(new AuraRouterModule($appDir . '/var/conf/aura.route.php'));
        $this->bind(MyLoggerInterface::class)->to(MyLogger::class);
+        $this->bindInterceptor(
+            $this->matcher->any(),                           // In any class,
+            $this->matcher->annotatedWith(BenchMark::class), // To #[BenchMark] attributed method
+            [BenchMarker::class]                             // Apply BenchMarker interceptor interception
+        );
        $this->install(new PackageModule());
    }
}

Apply the #[BenchMark] attribute to the method you want to benchmark.

+use MyVendor\Weekday\Annotation\BenchMark;

class Weekday extends ResourceObject
{

+   #[BenchMark]
    public function onGet(int $year, int $month, int $day): static
    {

Now you can benchmark any method by adding the #[BenchMark] attribute.

Adding functionality with attributes and interceptors is flexible. There are no changes to the target methods or the methods that call them. The attribute remains as is, but you can remove the binding if you don’t want to benchmark. For example, you can bind only during development and issue a warning if it exceeds a certain number of seconds.

Run it and check that the execution time logs are output to var/log/weekday.log.

php bin/app.php get '/weekday/2015/05/28'
cat var/log/cli-hal-api-app/weekday.log

HTML

Next, let’s turn this API application into an HTML application. In addition to the current app resource, add a page resource in src/Resource/Page/Index.php.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Resource\Page;

use BEAR\Resource\ResourceObject;
use BEAR\Resource\Annotation\Embed;
use BEAR\Resource\ResourceObject;

class Index extends ResourceObject
{
    #[Embed(rel:"_self", src: "app://self/weekday{?year,month,day}")]
    public function onGet(int $year, int $month, int $day): static
    {
        $this->body += [
            'year' => $year,
            'month' => $month,
            'day' => $day,
        ];

        return $this;
    }
}

The page resource class is essentially the same as the app resource class, just with different locations and roles. Linking with _self copies the app://self/weekday resource onto itself.

The app resource’s weekday is assigned to $body['weekday'], and the arguments year, month, day are added to the body.

Let’s see what representation this resource has.

php bin/page.php get '/?year=2000&month=1&day=1'
200 OK
Content-Type: application/hal+json

{
    "year": 2000,
    "month": 1,
    "day": 1,
    "weekday": "Sat",
    "_links": {
        "self": {
            "href": "/index?year=2000&month=1&day=1"
        }
    }
}

The resource is output as the application/hal+json media type, but to output it as HTML (text/html), install the HTML module. See HTML Manual.

composer install

composer require madapaja/twig-module ^2.0

Create src/Module/HtmlModule.php.

<?php
namespace MyVendor\Weekday\Module;

use Madapaja\TwigModule\TwigErrorPageModule;
use Madapaja\TwigModule\TwigModule;
use Ray\Di\AbstractModule;

class HtmlModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new TwigModule);
        $this->install(new TwigErrorPageModule);
    }
}

Copy the templates folder.

cp -r vendor/madapaja/twig-module/var/templates var

Change bin/page.php to use the html-app context.

<?php
use MyVendor\Weekday\Bootstrap;

require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())(PHP_SAPI === 'cli' ? 'cli-html-app' : 'html-app', $GLOBALS, $_SERVER));

This prepares you for text/html output. Finally, edit the var/templates/Page/Index.html.twig file.

{% extends 'layout/base.html.twig' %}
{% block title %}Weekday{% endblock %}
{% block content %}
The weekday of {{ year }}/{{ month }}/{{ day }} is {{ weekday }}.
{% endblock %}

Preparations are complete. First, check that this HTML is output in the console.

php bin/page.php get '/?year=1991&month=8&day=1'
200 OK
Content-Type: text/html; charset=utf-8

<!DOCTYPE html>
<html>
...

If html is not displayed at this time, there may be an error in the template engine. In that case, check the error in the log file (var/log/cli-html-app/last.log ref.log).

Next, to provide web services, also change public/index.php.

<?php

use MyVendor\Weekday\Bootstrap;

require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'html-app' : 'prod-html-app', $GLOBALS, $_SERVER));

Start the PHP server and check by accessing http://127.0.0.1:8080/?year=2001&month=1&day=1 in a web browser.

php -S 127.0.0.1:8080 public/index.php

Context is something like the application’s execution mode, and multiple can be specified. Let’s try it.

<?php

use MyVendor\Weekday\Bootstrap;

// JSON Application
require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())('prod-app', $GLOBALS, $_SERVER));
<?php

use MyVendor\Weekday\Bootstrap;

// Production HAL Application
require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())('prod-hal-app', $GLOBALS, $_SERVER));

PHP code that generates instances according to the context is created. Check the var/tmp/{context}/di folder of the application. You don’t usually need to see these files, but you can check how the objects are created.

REST API

Let’s create an application resource using sqlite3. First, create a DB in var/db/todo.sqlite3 in the console.

mkdir var/db
sqlite3 var/db/todo.sqlite3

sqlite> create table todo(id integer primary key, todo, created_at);
sqlite> .exit

You can choose database access from AuraSql, Doctrine Dbal, CakeDB, etc., but here we will install Ray.AuraSqlModule.

composer require ray/aura-sql-module

Install the module in src/Module/AppModule::configure(). At that time, bind DateTimeImmutable so that you can receive the current time in the constructor.

<?php
+use Ray\AuraSqlModule\AuraSqlModule;
+use DateTimeImmutable;

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        // ...
+        $this->bind(DateTimeImmutable::class);        
+        $this->install(new AuraSqlModule(sprintf('sqlite:%s/var/db/todo.sqlite3', $this->appMeta->appDir)));
        $this->install(new PackageModule());
    }
}

Place the Todo resource in src/Resource/App/Todos.php.

<?php

declare(strict_types=1);

namespace MyVendor\Weekday\Resource\App;

use Aura\Sql\ExtendedPdoInterface;
use BEAR\Package\Annotation\ReturnCreatedResource;
use BEAR\RepositoryModule\Annotation\Cacheable;
use BEAR\Resource\ResourceObject;
use DateTimeImmutable;
use Ray\AuraSqlModule\Annotation\Transactional;

use function sprintf;

#[Cacheable]
class Todos extends ResourceObject
{
    public function __construct(
        private readonly ExtendedPdoInterface $pdo,
        private readonly DateTimeImmutable $date,
    ) {
    }

    public function onGet(string $id = ''): static
    {
        $sql = $id ? /** @lang SQL */'SELECT * FROM todo WHERE id=:id' : /** @lang SQL */'SELECT * FROM todo';
        $this->body = $this->pdo->fetchAssoc($sql, ['id' => $id]);

        return $this;
    }

    #[Transactional, ReturnCreatedResource]
    public function onPost(string $todo): static
    {
        $this->pdo->perform(/** @lang SQL */'INSERT INTO todo (todo, created_at) VALUES (:todo, :created_at)', [
            'todo' => $todo,
            'created_at' => $this->date->format('Y-m-d H:i:s')
        ]);
        $this->code = 201; // Created
        $this->headers['Location'] = sprintf('/todos?id=%s', $this->pdo->lastInsertId()); // new URL

        return $this;
    }

    #[Transactional]
    public function onPut(int $id, string $todo): static
    {
        $this->pdo->perform(/** @lang SQL */'UPDATE todo SET todo = :todo WHERE id = :id', [
            'id' => $id,
            'todo' => $todo
        ]);
        $this->code = 204; // No content

        return $this;
    }
}

Pay attention to the attributes.

#[Cacheable]

The class attribute #[Cacheable] indicates that the GET method of this resource is cacheable.

#[Transactional]

#[Transactional] on onPost and onPut indicates database access transactions.

#[ReturnCreatedResource]

#[ReturnCreatedResource] on onPost creates and includes a resource indicated by the Location URL in the body. At this time, onGet is actually called using the Location header URI, ensuring the content of the Location header is correct while also creating a cache.

POST Request

Let’s try a POST.

First, to perform a cache-enabled test, create a test context boot file bin/test.php.

<?php

declare(strict_types=1);

use MyVendor\Weekday\Bootstrap;

require dirname(__DIR__) . '/autoload.php';
exit((new Bootstrap())('prod-cli-hal-api-app', $GLOBALS, $_SERVER));

Make a request with a console command. It’s a POST, but in BEAR.Sunday, parameters are passed in the form of a query.

php bin/test.php post '/todos?todo=shopping'
201 Created
Location: /todos?id=1

{
    "id": "1",
    "todo": "shopping",
    "created": "2017-06-04 15:58:03",
    "_links": {
        "self": {
            "href": "/todos?id=1"
        }
    }
}

The status code is 201 Created. The Location header indicates that a new resource has been created at /todos/?id=1. RFC7231 Section-6.3.2

Next, get this resource.

php bin/test.php get '/todos?id=1'
200 OK
ETag: 2527085682
Last-Modified: Sun, 04 Jun 2017 15:23:39 GMT
content-type: application/hal+json

{
    "id": "1",
    "todo": "shopping",
    "created": "2017-06-04 15:58:03",
    "_links": {
        "self": {
            "href": "/todos?id=1"
        }
    }
}

The Hypermedia API is complete! Let’s start the API server.

php -S 127.0.0.1:8081 bin/app.php

Use the curl command to GET.

curl -i 'http://127.0.0.1:8081/todos?id=1'
HTTP/1.1 200 OK
Host: 127.0.0.1:8081
Date: Sun, 02 May 2021 17:10:55 GMT
Connection: close
X-Powered-By: PHP/8.0.3
Content-Type: application/hal+json
ETag: 197839553
Last-Modified: Sun, 02 May 2021 17:10:55 GMT
Cache-Control: max-age=31536000

{
    "id": "1",

Make multiple requests and confirm that the Last-Modified date does not change. (Try adding echo or similar in the method to check.)

The Cacheable attribute, if not set with expiry, does not invalidate the cache over time. The cache is regenerated when resources are changed with onPut($id, $todo) or onDelete($id).

Next, change this resource with the PUT method.

curl -i http://127.0.0.1:8081/todos -X PUT -d "id=1&todo=think"

A 204 No Content response is returned, indicating there is no body.

HTTP/1.1 204 No Content
...

You can specify the media type with the Content-Type header. Try it with application/json as well.

curl -i http://127.0.0.1:8081/todos -X PUT -H 'Content-Type: application/json' -d '{"id": 1, "todo":"think" }'

GET again to see that the Etag and Last-Modified have changed.

curl -i 'http://127.0.0.1:8081/todos?id=1'

This Last-Modified date is provided by #[Cacheable]. There is no need for the application to manage this or provide a database column.

Using #[Cacheable], the resource content is managed in a “query repository” dedicated to resource storage, separate from the write database, and headers such as Etag and Last-Modified are automatically added.

Because Everything is A Resource.

In BEAR, everything is a resource.

Resource identifiers (URI), a unified interface, stateless access, powerful caching systems, hyperlinks, layered systems, and self-descriptiveness. BEAR.Sunday applications have these characteristics of REST, adhering to HTTP standards and excelling in reusability.

BEAR.Sunday is a connecting layer framework that ties dependencies with DI, cross-cutting concerns with AOP, and application information as resources with the power of REST.


```

  1. Normally, the vendor name is the name of an individual or team (organization). A GitHub account name or team name would be suitable. Enter the application name for project