BEAR.Sunday Complete Manual

This comprehensive manual contains all BEAR.Sunday documentation in a single page for easy reference, printing, or offline viewing.


Technology

The distinctive technologies and features of BEAR.Sunday are explained in the following chapters.

Architecture and Design Principles

Resource Oriented Architecture (ROA)

BEAR.Sunday’s ROA is an architecture that realizes RESTful API within a web application. It is the core of BEAR.Sunday’s design principles, functioning as both a hypermedia framework and a service-oriented architecture. Similar to the Web, all data and functions are considered resources and are operated through standardized interfaces such as GET, POST, PUT, and DELETE.

URI

URI (Uniform Resource Identifier) is a key element to the success of the Web and is also at the heart of BEAR.Sunday’s ROA. By assigning URIs to all resources handled by the application, resources can be easily identified and accessed. URIs not only function as identifiers for resources but also express links between resources.

Uniform Interface

Access to resources is done using HTTP methods such as GET, POST, PUT, and DELETE. These methods specify the operations that can be performed on resources and provide a common interface regardless of the type of resource.

Hypermedia

In BEAR.Sunday’s Resource Oriented Architecture (ROA), each resource provides affordances (available operations and functions for the client) through hyperlinks. These links represent the operations that clients can perform and guide navigation within the application.

Separation of State and Representation

In BEAR.Sunday’s ROA, the state of a resource and its representation are clearly separated. The state of the resource is managed by the resource class, and the renderer injected into the resource converts the state of the resource into a resource state representation in various formats (JSON, HTML, etc.). Domain logic and presentation logic are loosely coupled, and even with the same code, changing the binding of the state representation based on the context will also change the representation.

Differences from MVC

BEAR.Sunday’s ROA (Resource Oriented Architecture) takes a different approach from the traditional MVC architecture. MVC composes an application with three components: model, view, and controller. The controller receives a request object, controls a series of processes, and returns a response. In contrast, a resource in BEAR.Sunday, following the Single Responsibility Principle (SRP), only specifies the state of the resource in the request method and is not involved in the representation.

While there are no constraints on the relationship between controllers and models in MVC, resources have explicit constraints on including other resources using hyperlinks and URIs. This allows for declarative definition of content inclusion relationships and tree structures while maintaining information hiding of the called resources.

MVC controllers manually retrieve values from the request object, while resources declaratively define the required variables as arguments to the request method. Therefore, input validation is also performed declaratively using JsonSchema, and the arguments and their constraints are documented.

Dependency Injection (DI)

Dependency Injection (DI) is an important technique for enhancing the design and structure of applications in object-oriented programming. The central purpose of DI is to divide an application’s responsibilities into multiple components with independent domains or roles and manage the dependencies between them.

DI helps to horizontally divide one responsibility into multiple functions. The divided functions can be developed and tested independently as “dependencies”. By injecting those dependencies with clear responsibilities and roles based on the single responsibility principle from the outside, the reusability and testability of objects are improved. Dependencies can also be vertically divided into other dependencies, forming a tree of dependencies.

BEAR.Sunday’s DI uses a separate package called Ray.Di, which adopts the design philosophy of Google’s DI framework Guice and covers almost all of its features.

It also has the following characteristics:

  • Bindings can be changed by context, allowing different implementations to be injected during testing.
  • Attribute-based configuration enhances the self-descriptiveness of the code.
  • Ray.Di performs dependency resolution at compile-time, improving runtime performance. This is different from other DI containers that resolve dependencies at runtime.
  • Object dependencies can be visualized as a graph. Example: Root Object.

Ray.Di logo

Aspect Oriented Programming (AOP)

Aspect-Oriented Programming (AOP) is a pattern that realizes flexible applications by separating essential concerns such as business logic from cross-cutting concerns such as logging and caching. Cross-cutting concerns refer to functions or processes that span across multiple modules or layers. It is possible to bind cross-cutting processes based on search conditions and flexibly configure them based on context.

BEAR.Sunday’s AOP uses a separate package called Ray.Aop, which declaratively binds cross-cutting processes by attaching PHP attributes to classes and methods. Ray.Aop conforms to Java’s AOP Alliance.

AOP is often misunderstood as a technology that “has the strong power to break the existing order”. However, its raison d’être is not to exercise power beyond constraints but to complement areas where object-orientation is not well-suited, such as exploratory assignment of functions using matchers and separation of cross-cutting processes. AOP is a paradigm that can create cross-cutting constraints for applications, in other words, it functions as an application framework.

Performance and Scalability

ROA-based Event-Driven Content Strategy with Modern CDN Integration

BEAR.Sunday realizes an advanced event-driven caching strategy by integrating with instant purge-capable CDNs such as Fastly, with Resource Oriented Architecture (ROA) at its core. Instead of invalidating caches based on the conventional TTL (Time to Live), this strategy immediately invalidates the CDN and server-side caches, as well as ETags (entity tags), in response to resource state change events.

By taking this approach of creating non-volatile and persistent content on CDNs, it not only avoids SPOF (Single Point of Failure) and achieves high availability and fault tolerance but also maximizes user experience and cost efficiency. It realizes the same distributed caching as static content for dynamic content, which is the original principle of the Web. It re-realizes the scalable and network cost-reducing distributed caching principle that the Web has had since the 1990s with modern technology.

Cache Invalidation by Semantic Methods and Dependencies

In BEAR.Sunday’s ROA, each resource operation is given a semantic role. For example, the GET method retrieves a resource, and the PUT method updates a resource. These methods collaborate in an event-driven manner and efficiently invalidate related caches. For instance, when a specific resource is updated, the cache of resources that require that resource is invalidated. This ensures data consistency and freshness, providing users with the latest information.

Identity Confirmation and Fast Response with ETag

By setting ETags before the system boots, content identity can be quickly confirmed, and if there are no changes, a 304 Not Modified response is returned to minimize network load.

Partial Updates with Donut Caching and ESI

BEAR.Sunday adopts a donut caching strategy and uses ESI (Edge Side Includes) to enable partial content updates at the CDN edge. This technology allows for dynamic updates of only the necessary parts without re-caching the entire page, improving caching efficiency.

In this way, BEAR.Sunday and Fastly’s integration of ROA-based caching strategy not only realizes advanced distributed caching but also enhances application performance and fault tolerance.

Runtime Optimization

BEAR.Sunday is designed to minimize framework overhead regardless of server configuration.

DI resolution is completed at compile time, with no container lookups at runtime. The entire application is generated as a single root object variable and reused across requests. In php-fpm configurations, the pre-compiled dependency graph and opcache enable fast bootstrapping.

In Swoole configurations, persistent workers reduce bootstrapping to a single initial boot. Coroutine-context-based request isolation enables safe concurrent processing without reliance on superglobals. Combined with the transparent parallel execution described next, I/O wait time is further minimized.

Transparent Parallel Execution

In BEAR.Sunday, a URI expresses “intent” rather than being merely a communication protocol or locator. app://self/user expresses only the intent “I want user information”—whether it comes from MySQL or Redis is hidden from the application.

This complete separation of “What” from “How” enables multiple resources embedded with #[Embed] to be fetched in parallel without changing any application code. Resource classes written 10 years ago can benefit from parallel execution just by adding a Module.

Three tiers of solutions are available based on server environment constraints: ext-parallel (thread pool), Swoole (coroutines), and mysqli (DB queries only). Whichever you choose, application code requires no changes. Develop and debug with standard PHP, then switch to parallel execution in production with just a configuration change.

Developer Experience

Ease of Testing

BEAR.Sunday allows for easy and effective testing due to the following design features:

  • Each resource is independent, and testing is easy due to the stateless nature of REST requests. Since the state and representation of resources are clearly separated, it is possible to test the state of resources even when they are in HTML representation.
  • API testing can be performed while following hypermedia links, and tests can be written in the same code for PHP and HTTP.
  • Different implementations are bound during testing through context-based binding.

Application as Documentation

In BEAR.Sunday, the application itself is the documentation. Multiple documentation formats are automatically generated from code.

  • ApiDoc HTML: Developer reference
  • OpenAPI 3.1: Toolchain integration
  • JSON Schema: Information model definition
  • llms.txt: AI-readable application overview

When using an ALPS profile as the SSOT (Single Source of Truth), you define the application semantics (vocabulary, state transitions, operation meanings) first, then generate code from it. The same document holds different meanings for different readers—developers see endpoints, architects read state transitions, and AI extracts ontology.

Visualization and Debugging

Utilizing the technical feature of resources rendering themselves, during development, the scope of resources can be indicated on HTML, resource states can be monitored, and PHP code and HTML templates can be edited in an online editor and reflected in real-time.

Extensibility and Integration

Integration of PHP Interfaces and SQL Execution

In BEAR.Sunday, the execution of SQL statements for interacting with databases can be easily managed through PHP interfaces. It is possible to directly bind SQL execution objects to PHP interfaces without implementing classes. The boundary between the domain and infrastructure is connected by PHP interfaces.

In that case, types can also be specified for arguments, and any missing parts are dependency-resolved by DI and used as strings. Even when the current time is needed for SQL execution, there is no need to pass it; it is automatically bound. This helps keep the code concise as the client is not responsible for passing all arguments.

Moreover, direct management of SQL makes debugging easier when errors occur. The behavior of SQL queries can be directly observed, allowing for quick identification and correction of problems.

Integration with Other Systems

BEAR.Sunday resources can be accessed through various interfaces. In addition to web interfaces, resources can be accessed directly from the console, allowing the same resources to be used from both web and command-line interfaces without changing the source code. Furthermore, using BEAR.CLI, resources can be distributed as standalone UNIX commands. Multiple BEAR.Sunday applications can also run concurrently within the same PHP runtime, enabling collaboration between independent applications without building microservices.

Stream Output

By assigning streams such as file pointers to the body of a resource, large-scale content that cannot be handled in memory can be output. In that case, streams can also be mixed with ordinary variables, allowing flexible output of large-scale responses.

Gradual Migration from Other Systems

BEAR.Sunday provides a gradual migration path and enables seamless integration with other frameworks and systems such as Laravel and Symfony. This framework can be implemented as a Composer package, allowing developers to gradually introduce BEAR.Sunday’s features into their existing codebase.

Flexibility in Technology Migration

BEAR.Sunday protects investments in preparation for future technological changes and evolving requirements. Even if there is a need to migrate from this framework to another framework or language, the constructed resources will not go to waste. In a PHP environment, BEAR.Sunday applications can be integrated as Composer packages and continuously utilized, and BEAR.Thrift allows efficient access to BEAR.Sunday resources from other languages. When not using Thrift, access via HTTP is also possible. SQL code can also be easily reused.

Even if the library being used is strongly dependent on a specific PHP version, different versions of PHP can coexist using BEAR.Thrift.

Design Philosophy and Quality

Adoption of Standard Technologies and Elimination of Proprietary Standards

BEAR.Sunday has a design philosophy of adopting standard technologies as much as possible and eliminating framework-specific standards and rules. For example, it supports content negotiation for JSON format and www-form format HTTP requests by default and uses the vnd.error+json media type format for error responses. It actively incorporates standard technologies and specifications such as adopting HAL (Hypertext Application Language) for links between resources and using JsonSchema for validation.

On the other hand, it eliminates proprietary validation rules and framework-specific standards and rules as much as possible.

Object-Oriented Principles

BEAR.Sunday emphasizes object-oriented principles to make applications maintainable in the long term.

Composition over Inheritance

Composition is recommended over inheritance classes. Generally, directly calling a parent class’s method from a child class can potentially increase the coupling between classes. The only abstract class that requires inheritance at runtime by design is the resource class BEAR\Resource\ResourceObject, but the methods of ResourceObject exist solely for other classes to use. There is no case in BEAR.Sunday where a user calls a method of a framework’s parent class that they have inherited at runtime.

Everything is Injected

Framework classes do not refer to “configuration files” or “debug constants” during execution to determine their behavior. Dependencies corresponding to the behavior are injected. This means that to change the application’s behavior, there is no need to change the code; only the binding of the implementation of the dependency to the interface needs to be changed. Constants like APP_DEBUG or APP_MODE do not exist. There is no way to know in what mode the software is currently running after it has started, and there is no need to know.

Permanent Assurance of Backward Compatibility

BEAR.Sunday is designed with an emphasis on maintaining backward compatibility in the evolution of software and has continued to evolve without breaking backward compatibility since its release. In modern software development, frequent breaking of backward compatibility and the associated burden of modification and testing have become a challenge, but BEAR.Sunday has avoided this problem.

BEAR.Sunday not only adopts semantic versioning but also does not perform major version upgrades that involve breaking changes. It prevents new feature additions or changes to existing features from affecting existing code. Code that has become old and unused is given the attribute “deprecated” but is never deleted and does not affect the behavior of existing code. Instead, new features are added, and evolution continues.

Acyclic Dependencies Principle (ADP)

The Acyclic Dependencies Principle states that dependencies should be unidirectional and non-circular. The BEAR.Sunday framework adheres to this principle and is composed of a series of packages with a hierarchical structure where larger framework packages depend on smaller framework packages. Each level does not need to be aware of the existence of other levels that encompass it, and the dependencies are unidirectional and do not form cycles. For example, Ray.Aop is not even aware of the existence of Ray.Di, and Ray.Di is not aware of the existence of BEAR.Sunday.

Framework structure following the Acyclic Dependencies Principle

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.

The same applies to the runtime. When accessing a resource, first, the cross-cutting processing of the AOP aspects bound to the method is executed, and then the method determines the state of the resource. At this point, the method is not aware of the existence of the aspects bound to it. The same goes for resources embedded in the resource’s state. They do not have knowledge of the outer layers or elements. The separation of concerns is clearly defined.

Code Quality

To provide applications with high code quality, the BEAR.Sunday framework also strives to maintain a high standard of code quality.

  • The framework code is applied at the strictest level by both static analysis tools, Psalm and PHPStan.
  • It maintains 100% test coverage and nearly 100% type coverage.
  • It is fundamentally an immutable system and is so clean that initialization is not required every time, even in tests. It unleashes the power of PHP’s asynchronous communication engines like Swoole.

Architecture-Enabled Security Analysis

BEAR.Sunday’s architecture fundamentally simplifies security analysis.

Every endpoint is a ResourceObject with explicit onGet and onPost methods, inputs are declared via JSON Schema, and dependencies are explicit through constructor injection. With no hidden magic or global state, static analysis tools can trace complete data flows.

This declarative architecture enables multi-layered security scanning combining SAST (static analysis), DAST (dynamic testing), taint analysis, and AI auditing. Framework-aware specialized tools detect vulnerabilities that generic tools cannot find.

The Value BEAR.Sunday Brings

Value for Developers

  • Improved productivity: Based on robust design patterns and principles with constraints that don’t change over time, developers can focus on core business logic.
  • Collaboration in teams: By providing development teams with consistent guidelines and structure, it keeps the code of different engineers loosely coupled and unified, improving code readability and maintainability.
  • Flexibility and extensibility: BEAR.Sunday’s policy of not including libraries brings developers flexibility and freedom in component selection.
  • Ease of testing: BEAR.Sunday’s DI (Dependency Injection) and ROA (Resource Oriented Architecture) increase the ease of testing.

Value for Users

  • High performance: BEAR.Sunday’s runtime optimization and CDN-centric caching strategy brings users a fast and responsive experience.
  • Reliability and availability: BEAR.Sunday’s CDN-centric caching strategy minimizes single points of failure (SPOF), allowing users to enjoy stable services.
  • Ease of use: BEAR.Sunday’s excellent connectivity makes it easy to collaborate with other languages and systems.

Value for Business

  • Reduced development costs: The consistent guidelines and structure provided by BEAR.Sunday promote a sustainable and efficient development process, reducing development costs.
  • Reduced maintenance costs: BEAR.Sunday’s approach to maintaining backward compatibility increases technical continuity and minimizes the time and cost of change response.
  • High extensibility: With technologies like DI (Dependency Injection) and AOP (Aspect Oriented Programming) that change behavior while minimizing code changes, BEAR.Sunday allows applications to be easily extended in line with business growth and changes.
  • Excellent User Experience (UX): BEAR.Sunday provides high performance and high availability, increasing user satisfaction, enhancing customer loyalty, expanding the customer base, and contributing to business success.

Summary

Excellent constraints do not change. The constraints brought by BEAR.Sunday provide specific value to developers, users, and businesses respectively.

BEAR.Sunday is a framework designed based on the principles and spirit of the Web, providing developers with clear constraints to empower them to build flexible and robust applications.


Version

Supported PHP

Continuous Integration

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)

  • 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, you can check the tests for each version in CI.

Semver

BEAR.Sunday follows Semantic Versioning. 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

VENDOR=MyVendor PACKAGE=MyProject composer create-project bear/skeleton my-project
cd my-project

Use VENDOR to specify the vendor name and PACKAGE to specify the package name for installation. If not specified, you will be prompted interactively.

Next, let’s create a new resource. A resource is a class which corresponds, for instance, to a JSON payload (if working with an API-first driven model) or a web page. Create your own basic page resource in src/Resource/Page/Hello.php

<?php
namespace MyVendor\MyProject\Resource\Page;

use BEAR\Resource\ResourceObject;

class Hello extends ResourceObject
{
    public function onGet(string $name = 'BEAR.Sunday'): static
    {
        $this->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.

php bin/page.php get /hello
php bin/page.php get '/hello?name=World'
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.

php -S 127.0.0.1:8080 -t public
curl -i 127.0.0.1:8080/hello

Environment Setup

Choose malt / Docker / manual setup based on your OS and team structure. This is a practical guide that consolidates the features, setup procedures, and operational points for each method in one place.


Method Selection

Method Target OS Features Recommended Use
malt macOS, WSL2, Linux Homebrew-based, lightweight, configuration shareable, local-complete
Batch service management commands
Individual dev, team dev
Docker macOS, Windows, Linux Container-based complete environment reproduction, CI/CD friendly Team dev, CI/CD, production-like
Manual All OS Use existing environment as-is, fine-grained control Existing infrastructure, constrained environments

Environment Setup with malt

Overview

malt is a development environment management tool based on Homebrew. It consolidates configuration and data directly under the project, achieving local completion.

Key Features

  • Completely Local: All configuration and data stored within the project
  • Clean Deletion: Folder deletion = environment deletion
  • Dedicated Port Commands: Aliases like mysql@3306 / redis@6379
  • No Global Pollution: No impact on system MySQL/Redis etc.
  • Configuration Visibility: Configuration files can be shared and reviewed within the project
  • Batch Service Management: malt start / malt stop can start/stop related services together

Prerequisites

  • macOS or Linux (including WSL2)
  • Homebrew installed

Installation

# Add Homebrew taps
brew tap shivammathur/php
brew tap shivammathur/extensions
brew tap koriym/malt

# Install malt
brew install malt

Basic Operations (Shortest Path)

malt init && malt install && malt create && malt start
source <(malt env)

Configuration Files

malt.json          # malt configuration
malt/
  conf/
    my_3306.cnf     # MySQL configuration
    php.ini         # PHP configuration
    httpd_8080.conf # Apache configuration
    nginx_80.conf   # Nginx configuration

These files can be included in your project for team environment sharing.

Service Management

# Status check
malt status

# Start / stop / restart all services
malt start
malt stop
malt restart

# Specific services only
malt start mysql
malt stop nginx

Database Operations

mysql@3306  # Connect to project-specific MySQL
redis@6379  # Connect to project-specific Redis
mysql@3306 -e "CREATE DATABASE IF NOT EXISTS myapp"

Important: mysql@3306 is project-specific connection. It’s isolated from the system’s global MySQL.


Environment Setup with Docker

Overview

Docker provides OS-independent, consistent development environments.

Docker Considerations:

  • Global Command Conflicts: The system mysql command points to global MySQL installation
  • Container-specific Access: Requires specific connection methods for Docker container databases
  • Port Conflict Risk: Ports like 3306 may conflict with system services
  • macOS File Access: Host-container file mount performance degradation, especially noticeable during bulk file operations (builds, tests)
  • Security: MYSQL_ALLOW_EMPTY_PASSWORD should be limited to development use only

Prerequisites

Basic docker-compose.yml

version: '3.8'

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ""
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
      MYSQL_DATABASE: myapp
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
    command: --default-authentication-plugin=mysql_native_password
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 3
    
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"
      
  memcached:
    image: memcached:alpine
    ports:
      - "11211:11211"

volumes:
  mysql_data:

Usage

# Start environment
docker-compose up -d

# Check status
docker-compose ps

# Check logs
docker-compose logs mysql

# Stop environment
docker-compose stop

# Complete removal (including data)
docker-compose down -v

Database Connection

# Connect from host (port specification required)
mysql -h 127.0.0.1 -P 3306 -u root

# Connect from within container (recommended)
docker-compose exec mysql mysql -u root

Warning: If mysql is installed on your system, running just mysql will connect to your system’s MySQL, not the Docker container. To access the Docker database, you must either specify host/port or execute from within the container.


Manual Environment Setup

PHP

# macOS (Homebrew)
brew install php@8.4
brew install composer

# Ubuntu/Debian
sudo apt update
sudo apt install php8.4 php8.4-{cli,mysql,mbstring,xml,zip,curl}
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

# CentOS/RHEL
sudo dnf install php php-{cli,mysql,mbstring,xml,zip,curl}

MySQL

# macOS (Homebrew)
brew install mysql@8.0
brew services start mysql@8.0

# Ubuntu/Debian
sudo apt install mysql-server-8.0
sudo systemctl start mysql

# CentOS/RHEL
sudo dnf install mysql-server
sudo systemctl start mysqld

Useful PHP Extensions for Development

# Xdebug (debugging)
brew install shivammathur/extensions/xdebug@8.4    # Homebrew
sudo apt install php8.4-xdebug                    # Ubuntu

# XHProf (profiling)
brew install shivammathur/extensions/xhprof@8.4   # Homebrew
sudo apt install php8.4-xhprof                    # Ubuntu

# Redis
brew install shivammathur/extensions/redis@8.4    # Homebrew
sudo apt install php8.4-redis                     # Ubuntu

# APCu (caching)
brew install shivammathur/extensions/apcu@8.4     # Homebrew
sudo apt install php8.4-apcu                      # Ubuntu

Important: Xdebug and XHProf impact performance, so avoid leaving them enabled all the time. When you configure them, Xdebug uses zend_extension=xdebug.so, while XHProf uses extension=xhprof.so; enable them from the CLI only when needed.

# Enable Xdebug only when debugging
php -dzend_extension=xdebug.so -S 127.0.0.1:8080 -t public

# Enable XHProf only when profiling
php -dextension=xhprof.so script.php

# Disable Xdebug when running Composer (recommended)
XDEBUG_MODE=off composer install

# Or override the PHP ini setting
php -dxdebug.mode=off /usr/local/bin/composer install

BEAR.Sunday Quick Start Example

composer create-project bear/skeleton my-app
cd my-app
malt init && malt install && malt create && malt start
source <(malt env)

Project-specific Configuration

.env (Example)

# MySQL
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=myapp
DB_USER=root
DB_PASS=
DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=myapp

# SQLite (switching example)
# DB_DSN=sqlite:var/db.sqlite3

# Redis
REDIS_HOST=127.0.0.1:6379

# Memcached
MEMCACHED_HOST=127.0.0.1:11211

Migration (Phinx Example)

composer require --dev robmorgan/phinx
./vendor/bin/phinx init
./vendor/bin/phinx create MyMigration
./vendor/bin/phinx migrate

Development Server

PHP Built-in Server

# Start on port 8080
php -S 127.0.0.1:8080 -t public

# Enable Xdebug only when debugging
php -dzend_extension=xdebug.so -S 127.0.0.1:8080 -t public

malt Server

# Choose Apache / Nginx and start
malt start apache   # http://127.0.0.1:8080
malt start nginx    # http://127.0.0.1:80

# Check services
malt status

# Start/stop all services
malt start
malt stop

# Individual stop
malt stop apache
malt stop nginx

Troubleshooting

Port Conflicts

# macOS/Linux
lsof -i :3306
netstat -tulpn | grep :3306

# Kill the process
kill -9 <PID>

PHP Configuration Check

php --ini     # Loaded configuration
php -m        # Loaded modules

MySQL Connection Errors

# Connection test
mysql -h 127.0.0.1 -P 3306 -u root -p

# Linux service status
sudo systemctl status mysql

# Error logs
sudo tail -f /var/log/mysql/error.log

malt-specific Issues

# Status check
malt status

# Reset configuration
malt stop
rm -rf malt/
malt create
malt start

.gitignore for Team Development

malt/logs/
malt/data/
malt/tmp/

CI/CD (GitHub Actions Example)

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: ""
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping" 
          --health-interval=10s 
          --health-timeout=5s 
          --health-retries=3

    steps:
      - uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, xml, pdo_mysql, mysqli, intl, curl, zip

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - name: Run tests
        run: ./vendor/bin/phpunit

Environment Selection Guidelines

Development environments prioritize transparency and direct access, while production environments prioritize reproducibility and monitoring capabilities.

  • Daily Development & Learning: malt (instant mysql@3306, visible configuration, fast file access)
  • Team Development: malt (configuration sharing) or Docker (reproducibility priority)
  • Production & CI/CD: Docker only (same behavior anywhere, rich monitoring tool ecosystem)
  • Complex Configurations: Docker (assuming integration and scale of dependent services)

Follow your project’s tutorials and team conventions for detailed configuration and BEAR.Sunday best practices.


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.

VENDOR=MyVendor PACKAGE=Weekday composer create-project bear/skeleton weekday
cd weekday

1

Resources

BEAR.Sunday applications are made up of resources. A ResourceObject is an object that represents a web resource itself. It receives HTTP requests and transforms itself into the current state of that resource.

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 = 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.

The job of a ResourceObject is to receive requests and determine its own state.

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. HAL+JSON is a JSON format that uses the _links section to link related resources. For more details about HAL+JSON, see here.

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 = 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 = 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 testInvalidDateTime(): 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
    {
        $dateTime = DateTimeImmutable::createFromFormat('Y-m-d', "$year-$month-$day");
        $weekday = $dateTime->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;

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.



***

# Tutorial 2


In this tutorial, you will learn how to develop high quality standards-based REST (Hypermedia) applications using the following tools.

* Define a JSON schema and use it for validation and documentation [Json Schema](https://json-schema.org/)
* Hypermedia types [HAL (Hypertext Application Language)](https://stateless.group/hal_specification.html)
* A DB migration tool developed by CakePHP [Phinx](https://book.cakephp.org/phinx/0/en/index.html)
* Binding PHP interfaces to SQL statement execution [Ray. MediaQuery](https://github.com/ray-di/Ray.MediaQuery)

Let's proceed with the commits found in [tutorial2](https://github.com/bearsunday/tutorial2/commits/v2-php8.2).

## Create the project

Create the project skeleton.

```bash
VENDOR=MyVendor PACKAGE=Ticket composer create-project bear/skeleton ticket
cd ticket

Migration

Install Phinx.

composer require --dev robmorgan/phinx

Configure the DB connection information in the .env.dist file in the project root folder.

TKT_DB_HOST=127.0.0.1:3306
TKT_DB_NAME=ticket
TKT_DB_USER=root
TKT_DB_PASS=''
TKT_DB_SLAVE=''
TKT_DB_DSN=mysql:host=${TKT_DB_HOST}

The .env.dist file should look like this, and the actual connection information should be written in .env. 2

Next, create a folder to be used by Phinx.

mkdir -p var/phinx/migrations
mkdir var/phinx/seeds

Set up var/phinx/phinx.php to use the .env connection information we have set up earlier.

<?php
use BEAR\Dotenv\Dotenv;

require_once dirname(__DIR__, 2) . '/vendor/autoload.php';

(new Dotenv())->load(dirname(__DIR__, 2));

$development = new PDO(getenv('TKT_DB_DSN'), getenv('TKT_DB_USER'), getenv('TKT_DB_PASS'));
$test = new PDO(getenv('TKT_DB_DSN') . '_test', getenv('TKT_DB_USER'), getenv('TKT_DB_PASS'));
return [
    'paths' => [
        'migrations' => __DIR__ . '/migrations',
    ],
    'environments' => [
        'development' => [
            'name' => $development->query("SELECT DATABASE()")->fetchColumn(),
            'connection' => $development
        ],
        'test' => [
            'name' => $test->query("SELECT DATABASE()")->fetchColumn(),
            'connection' => $test
        ]
    ]
];

setup script

Edit bin/setup.php for easy database creation and migration.

<?php
use BEAR\Dotenv\Dotenv;

require_once dirname(__DIR__) . '/vendor/autoload.php';

(new Dotenv())->load(dirname(__DIR__));

chdir(dirname(__DIR__));
passthru('rm -rf var/tmp/*');
passthru('chmod 775 var/tmp');
passthru('chmod 775 var/log');
// db
$pdo = new \PDO('mysql:host=' . getenv('TKT_DB_HOST'), getenv('TKT_DB_USER'), getenv('TKT_DB_PASS'));
$pdo->exec('CREATE DATABASE IF NOT EXISTS ' . getenv('TKT_DB_NAME'));
$pdo->exec('DROP DATABASE IF EXISTS ' . getenv('TKT_DB_NAME') . '_test');
$pdo->exec('CREATE DATABASE ' . getenv('TKT_DB_NAME') . '_test');
passthru('./vendor/bin/phinx migrate -c var/phinx/phinx.php -e development');
passthru('./vendor/bin/phinx migrate -c var/phinx/phinx.php -e test');

Next, we will create a migration class to create the ticket table.

./vendor/bin/phinx create Ticket -c var/phinx/phinx.php
Phinx by CakePHP - https://phinx.org.

...
created var/phinx/migrations/20210520124501_ticket.php

Edit var/phinx/migrations/{current_date}_ticket.php to implement the change() method.

<?php
use Phinx\Migration\AbstractMigration;

final class Ticket extends AbstractMigration
{
    public function change(): void
    {
        $table = $this->table('ticket', ['id' => false, 'primary_key' => ['id']]);
        $table->addColumn('id', 'uuid', ['null' => false])
            ->addColumn('title', 'string')
            ->addColumn('date_created', 'datetime')
            ->create();
    }
}

In addition, edit .env.dist like the following.

 TKT_DB_USER=root
 TKT_DB_PASS=
 TKT_DB_SLAVE=
-TKT_DB_DSN=mysql:host=${TKT_DB_HOST}
+TKT_DB_DSN=mysql:host=${TKT_DB_HOST};dbname=${TKT_DB_NAME}

Now that we are done with the setup, run the setup command to create the table.

composer setup
> php bin/setup.php
...
All Done. Took 0.0248s

The table has been created. The next time you want to set up a database environment for this project, just run composer setup.

For more information about writing migration classes, see Phinx Manual: Writing Migrations.

Module

Install the module as a composer.

composer require ray/identity-value-module ray/media-query -w

Install the package with AppModule.

src/Module/AppModule.php

<?php
namespace MyVendor\Ticket\Module;

use BEAR\Dotenv\Dotenv;
use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;

use BEAR\Resource\Module\JsonSchemaModule;
use Ray\AuraSqlModule\AuraSqlModule;
use Ray\IdentityValueModule\IdentityValueModule;
use Ray\MediaQuery\DbQueryConfig;
use Ray\MediaQuery\MediaQueryModule;
use Ray\MediaQuery\Queries;
use function dirname;

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        (new Dotenv())->load(dirname(__DIR__, 2));
        $this->install(
            new AuraSqlModule(
                (string) getenv('TKT_DB_DSN'),
                (string) getenv('TKT_DB_USER'),
                (string) getenv('TKT_DB_PASS'),
                (string) getenv('TKT_DB_SLAVE')
            )
        );
        $this->install(
            new MediaQueryModule(
                Queries::fromDir($this->appMeta->appDir . '/src/Query'), [
                   new DbQueryConfig($this->appMeta->appDir . '/var/sql'),
                ]
            )
        );
        $this->install(new IdentityValueModule());
        $this->install(
            new JsonSchemaModule(
                $this->appMeta->appDir . '/var/schema/response',
                $this->appMeta->appDir . '/var/schema/request'
            )
        );
        $this->install(new PackageModule());
    }
}

SQL

Save the three SQLs for the ticket in var/sql.3

var/sql/ticket_add.sql

/* ticket add */
INSERT INTO ticket (id, title, date_created)
VALUES (:id, :title, :dateCreated);

var/sql/ticket_list.sql

/* ticket list */
SELECT id, title, date_created
  FROM ticket
 LIMIT 3;

var/sql/ticket_item.sql

/* ticket item */
SELECT id, title, date_created
  FROM ticket
 WHERE id = :id

Make sure that the SQL will work on its own when you create it.

PHPStorm includes a database tool, DataGrip, which has all the necessary features for SQL development such as code completion and SQL refactoring. Once the DB connection and other setups are made, SQL files can be executed directly in the IDE. 45

JsonSchema.

Create new files that will represent the resource Ticket (ticket item) and Tickets (ticket item list) with JsonSchema:

var/schema/response/ticket.json

{
  "$id": "ticket.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Ticket",
  "type": "object",
  "required": ["id", "title", "dateCreated"],
  "properties": {
    "id": {
      "description": "The unique identifier for a ticket.",
      "type": "string",
      "maxLength": 64
    },
    "title": {
      "description": "The unique identifier for a ticket.",
      "type": "string",
      "maxLength": 255
    },
    "dateCreated": {
      "description": "The date and time that the ticket was created",
      "type": "string",
      "format": "datetime"
    }
  }
}

var/schema/response/tickets.json

Tickets is a Ticket array.

{
  "$id": "tickets.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Tickets",
  "type": "object",
  "required": ["tickets"],
  "properties": {
    "tickets": {
      "type": "array",
      "items":{"$ref": "./ticket.json"}
    }
  }
}

  • $id - specifies the file name, but if it is to be published, it should be a URL.
  • title - This will be treated in the API documentation as an object name.
  • examples - specify examples as appropriate. You can also specify the entire object.

In PHPStorm, you will see a green check in the upper right corner of the editor to indicate that everything is OK. You should also validate the schema itself when you create it.

Query Interface

We will create a PHP interface that abstracts access to the infrastructure.

  • Read Ticket resources TicketQueryInterface.
  • Create a Ticket resource TicketCommandInterface.

src/Query/TicketQueryInterface.php

<?php

namespace MyVendor\Ticket\Query;

use Ray\MediaQuery\Annotation\DbQuery;

interface TicketQueryInterface
{
    #[DbQuery('ticket_item')]
    public function item(string $id): Ticket|null;

    /** @return array<Ticket> */
    #[DbQuery('ticket_list')]
    public function list(): array;
}

src/Query/TicketCommandInterface.php

<?php

namespace MyVendor\Ticket\Query;

use DateTimeInterface;
use Ray\MediaQuery\Annotation\DbQuery;

interface TicketCommandInterface
{
    #[DbQuery('ticket_add')]
    public function add(string $id, string $title, DateTimeInterface $dateCreated = null): void;
}

Specify an SQL statement with the #[DbQuery] attribute. You do not need to write any implementation for this interface. An object that performs the specified SQL query will be created automatically.

The interface is divided into two concerns: command which has side effects, and query which returns a value. It can be one interface and one method as in ADR pattern. The application designer decides the policy.

Entity

If you specify array for the return value of a method, you will get the database result as it is, an associative array, but if you specify an entity type for the return value of the method, it will be hydrated to that type.

#[DbQuery('ticket_item')]
public function item(string $id): array // you get an array.
#[DbQuery('ticket_item')]
public function item(string $id): Ticket|null; // yields a Ticket entity.

For multiple rows (row_list), use /** @return array<Ticket>*/ and phpdoc to specify that Ticket is returned as an array.

/** @return array<Ticket> */
#[DbQuery('ticket_list')]
public function list(): array; // yields an array of Ticket entities.

The value of each row is passed to the constructor by name argument. 6

<?php

declare(strict_types=1);

namespace MyVendor\Ticket\Entity;

class Ticket
{
    public readonly string $dateCreated;

    public function __construct(
        public readonly string $id,
        public readonly string $title,
        string $date_created
    ) {
        $this->dateCreated = $date_created;
    }
}

Databases use snake_case (date_created) while JSON uses camelCase (dateCreated) naming conventions. For multi-word property names, explicit conversion in the constructor resolves the gap between database and JSON naming cultures.

Resources

The resource class depends on the query interface.

Ticket resource

Create a ticket resource in src/Resource/App/Ticket.php.

<?php

declare(strict_types=1);

namespace MyVendor\Ticket\Resource\App;

use BEAR\Resource\Annotation\JsonSchema;
use BEAR\Resource\ResourceObject;
use MyVendor\Ticket\Query\TicketQueryInterface;

class Ticket extends ResourceObject
{
    public function __construct(
        private TicketQueryInterface $query
    ){}
    
   #[JsonSchema("ticket.json")]
   public function onGet(string $id = ''): static
    {
        $this->body = (array) $this->query->item($id);

        return $this;
    }
}

The attribute #[JsonSchema] indicates that the value output by onGet() is defined in the ticket.json schema. It is validated for each request by AOP.

Let’s try to request a resource by entering a seed. 7

% mysql -u root -e "INSERT INTO ticket (id, title, date_created) VALUES ('1', 'foo', '1970-01-01 00:00:00')" ticket
% php bin/app.php get '/ticket?id=1'
200 OK
Content-Type: application/hal+json

{
    "id": "1",
    "title": "foo",
    "date_created": "1970-01-01 00:00:01",
    "_links": {
        "self": {
            "href": "/ticket?id=1"
        }
    }
}

MediaQuery

With Ray.MediaQuery, an auto-generated SQL execution object is injected from the interface without the need to code boilerplate implementation classes. 8

A SQL statement can contain multiple SQLs separated by ;, and multiple SQLs are bound to the same parameter by name, and transactions are executed for queries other than SELECT.

If you want to generate SQL dynamically, you can use an SQL execution class that injects the query builder instead of Ray. For more details, please see Database in the manual.

Usually, a website page contains multiple resources. For example, a blog post page might contain recommendations, advertisements, category links, etc. in addition to the post. Instead of the client getting them separately, they can be bundled into one resource with embedded links as independent resources.

Think of HTML and the <img> tag that is written in it. Both have independent URLs, but the image resource is embedded in the HTML resource, and when the HTML is retrieved, the image is displayed in the HTML. These are called hypermedia types Embedding links(LE), and the resource to be embedded is linked.

Let’s embed the project resource into the ticket resource, and prepare the Project class.

src/Resource/App/Project.php

<?php

namespace MyVendor\Ticket\Resource\App;

use BEAR\Resource\ResourceObject;

class Project extends ResourceObject
{
    public function onGet(): static
    {
        $this->body = ['title' => 'Project A'];

        return $this;
    }
}

Add the attribute #[Embed] to the Ticket resource.

+use BEAR\Resource\Annotation\Embed;
+use BEAR\Resource\Request;
+
+   #[Embed(src: '/project', rel: 'project')]
    #[JsonSchema("ticket.json")]
    public function onGet(string $id = ''): static
    {
+        assert($this->body['project'] instanceof Request);
-        $this->body = (array) $this->query->item($id);
+        $this->body += (array) $this->query->item($id);

The request for the resource specified by the #[Embed] attribute src will be injected into the rel key of the body property, and will be lazily evaluated into a string representation when rendered.

For the sake of simplicity, no parameters are passed in this example, but you can pass the values received by the method arguments using the URI template, or you can modify or add parameters to the injected request. See resource for details.

If you make the request again, you will see that the status of the project resource has been added to the property _embedded.

% php bin/app.php get '/ticket?id=1'

{
    "id": "1",
    "title": "2",
    "date_created": "1970-01-01 00:00:01",
+    "_embedded": {
+        "project": {
+            "title": "Project A",
+        }
    },

Embedded resources are an important feature of the REST API. It gives a tree structure to the content and reduces the HTTP request cost. Instead of letting the client fetching it as a separate resource each time, the relationship can be represented in server-side. 9

tickets resource

Create a tickets resource in src/resource/App/Tickets.php that can be created with POST and retrieved with GET for a list of tickets.

<?php

declare(strict_types=1);

namespace MyVendor\Ticket\Resource\App;

use BEAR\Resource\Annotation\JsonSchema;
use BEAR\Resource\Annotation\Link;
use BEAR\Resource\ResourceObject;
use Koriym\HttpConstants\ResponseHeader;
use Koriym\HttpConstants\StatusCode;
use MyVendor\Ticket\Query\TicketCommandInterface;
use MyVendor\Ticket\Query\TicketQueryInterface;
use Ray\IdentityValueModule\UuidInterface;
use function uri_template;

class Tickets extends ResourceObject
{
    public function __construct(
        private TicketQueryInterface $query,
        private TicketCommandInterface $command,
        private UuidInterface $uuid
    ){}

    #[Link(rel: "doPost", href: '/tickets')]
    #[Link(rel: "goTicket", href: '/ticket{?id}')]
    #[JsonSchema("tickets.json")]
    public function onGet(): static
    {
        $this->body = [
            'tickets' => $this->query->list()
        ];
        
        return $this;
    }

    #[Link(rel: "goTickets", href: '/tickets')]
    public function onPost(string $title): static
    {
        $id = (string) $this->uuid;
        $this->command->add($id, $title);

        $this->code = StatusCode::CREATED;
        $this->headers[ResponseHeader::LOCATION] = uri_template('/ticket{?id}', ['id' => $id]);

        return $this;
    }
}

The injected $uuid can be cast to a string to get the UUID. Also, #Link[] represents a link to another resource (application state).

Notice that we don’t pass the current time in the add() method. If no value is passed, it will not be null, but the MySQL current time string will be bound to the SQL. This is because the string representation of the current time DateTime object bound to the DateTimeInterface (current time string) is bound to SQL.

public function add(string $id, string $title, DateTimeInterface $dateCreated = null): void;

It saves you the trouble of hard-coding NOW() inside SQL and passing the current time to the method every time. You can pass a DateTime object, or in the context of a test, you can bind a fixed test time.

In this way, if you specify an interface as an argument to a query, you get that object using DI, and its string representation is bound to SQL. For example, login user IDs can be bound and used across applications. 10

Hypermedia API test

The term REST (representational state transfer) was introduced and defined by Roy Fielding in his doctoral dissertation in 2000, and is intended to give an idea of “the behavior of a properly designed web application”. It is a network of web resources (a virtual state machine) where the user selects a resource identifier (URL) and a resource operation (application state transition) such as GET or POST to proceed with the application, resulting in the next representation of the resource (the next application state) being forwarded to the end user. application state) is transferred to the end user for use.

Wikipedia (REST)

In a REST application, the following actions are provided by the service as URLs, and the client selects them.

HTML web applications are completely RESTful. The only operations are “Go to the provided URL (with a tag, etc.)” or “Fill the provided form and submit”.

The REST API tests are written in the same way.

<?php

declare(strict_types=1);

namespace MyVendor\Ticket\Hypermedia;

use BEAR\Resource\ResourceInterface;
use BEAR\Resource\ResourceObject;
use Koriym\HttpConstants\ResponseHeader;
use MyVendor\Ticket\Injector;
use MyVendor\Ticket\Query\TicketQueryInterface;
use PHPUnit\Framework\TestCase;
use Ray\Di\InjectorInterface;
use function json_decode;

class WorkflowTest extends TestCase
{
    protected ResourceInterface $resource;
    protected InjectorInterface $injector;

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

    public function testIndex(): static
    {
        $index = $this->resource->get('/');
        $this->assertSame(200, $index->code);

        return $index;
    }

    /**
     * @depends testIndex
     */
    public function testGoTickets(ResourceObject $response): static
    {

        $json = (string) $response;
        $href = json_decode($json)->_links->{'goTickets'}->href;
        $ro = $this->resource->get($href);
        $this->assertSame(200, $ro->code);

        return $ro;
    }

    /**
     * @depends testGoTickets
     */
    public function testDoPost(ResourceObject $response): static
    {
        $json = (string) $response;
        $href = json_decode($json)->_links->{'doPost'}->href;
        $ro = $this->resource->post($href, ['title' => 'title1']);
        $this->assertSame(201, $ro->code);

        return $ro;
    }

    /**
     * @depends testDoPost
     */
    public function testGoTicket(ResourceObject $response): static
    {
        $href = $response->headers[ResponseHeader::LOCATION];
        $ro = $this->resource->get($href);
        $this->assertSame(200, $ro->code);

        return $ro;
    }
}

You will also need a route page as a starting point.

src/Resource/App/Index.php

<?php

declare(strict_types=1);

namespace MyVendor\Ticket\Resource\App;

use BEAR\Resource\Annotation\Link;
use BEAR\Resource\ResourceObject;

class Index extends ResourceObject
{
    #[Link(rel: 'goTickets', href: '/tickets')]
    public function onGet(): static
    {
        return $this;
    }
}
  • setUp creates a resource client, and testIndex() accesses the root page.
  • The testGoTickets() method, which receives the response, makes a JSON representation of the response object and gets the link goTickets to get the next list of tickets.
  • There is no need to write a test for the resource body. * No need to write tests for the resource body, just check the status code, since it is guaranteed that the JsonSchema validation of the response has passed.
  • Following the uniform interface of REST, the next request URL to be accessed is always included in the response. Inspect them one after another.

Uniform Interface

REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state.11

Let’s run it.

./vendor/bin/phpunit --testsuite hypermedia

Hypermedia API tests (REST application tests) are a good representation of the fact that REST applications are state machines, and workflows can be described as use cases. Ideally, REST API tests should cover how the application will be used.

HTTP Testing

To test the REST API over HTTP, inherit the whole test and set the client to the HTTP test client with setUp.

class WorkflowTest extends Workflow
{
    protected function setUp(): void
    {
        $this->resource = new HttpResource('127.0.0.1:8080', __DIR__ . '/index.php', __DIR__ . '/log/workflow.log');
    }
}

This client has the same interface as the resource client, but the actual request is made as an HTTP request to the built-in server and receives the response from the server. The first argument is the URL of the built-in server. When new is executed, the built-in server will be started with the bootstrap script specified in the second argument.

The bootstrap script for the test server will also be changed to the API context.

tests/Http/index.php

-exit((new Bootstrap())('hal-app', $GLOBALS, $_SERVER));
+exit((new Bootstrap())('hal-api-app', $GLOBALS, $_SERVER));

Let’s run it.

./vendor/bin/phpunit --testsuite http

HTTP Access Log

The actual HTTP request/response log made by curl will be recorded in the resource log of the third argument.

curl -s -i 'http://127.0.0.1:8080/'

HTTP/1.1 200 OK
Host: 127.0.0.1:8080
Date: Fri, 21 May 2021 22:41:02 GMT
Connection: close
X-Powered-By: PHP/8.0.6
Content-Type: application/hal+json

{
    "_links": {
        "self": {
            "href": "/index"
        },
        "goTickets": {
            "href": "/tickets"
        }
    }
}
curl -s -i -H 'Content-Type:application/json' -X POST -d '{"title":"title1"}' http://127.0.0.1:8080/tickets

HTTP/1.1 201 Created
Host: 127.0.0.1:8080
Date: Fri, 21 May 2021 22:41:02 GMT
Connection: close
X-Powered-By: PHP/8.0.6
Location: /ticket?id=421d997c-9a0e-4018-a6c2-9b8758cac6d6

The actual recorded JSON is useful for checking, especially if it has a complex structure, and is also good to check along with the API documentation. The HTTP client can also be used for E2E testing.

API documentation

In ResourceObjects, method signatures are the input parameters to the API, and responses are schema-defined. Because of its self-descriptiveness, API documentation can be generated automatically.

Let’s create it. The documentation will be output to the docs folder.

composer doc

It reduces the effort of writing IDL (Interface Definition Language), but more valuable is that the documentation follows the latest PHP code and is always accurate. It is a good idea to include it in your CI so that your code and API documentation are always in sync.

You can also link to related documentation. See ApiDoc for more details on configuration.

Code examples

The following code example is also available.

  • TestModulethat adds a Test context and clears the DB for each test. 4e9704d
  • entity option for #[DbQuery] that returns a hydrated entity class instead of an associative array in DB queries 29f0a1f
  • Query builder synthesizing static and dynamic SQL 9d095ac

REST framework

There are three styles of Web APIs.

  • Tunnels (SOAP, GraphQL)
  • URI (Object, CRUD)
  • Hypermedia (REST)

In contrast to the URI style, where resources are treated as just RPCs 12, what we learned in this tutorial is REST, where resources are linked. 13 Resources are connected by LOs (outbound links) in #Link to represent workflows, and LEs (embedded links) in #[Embed] to represent tree structures.

BEAR.Sunday emphasizes clean, standards-based code.

JsonSchema over framework-specific validators, standard SQL over proprietary ORM, IANA registered standard14 media type JSON over proprietary structure JSON.

Application design is not about “free implementation”, but about “free choice of constraints”. Applications should aim for evolvability without breaking development efficiency, performance, and backward compatibility based on the constraints.

(This manual has been prepared through deepL automated translation.)


The comment is not only descriptive, but also makes it easier to identify the SQL in the slow query log, etc.


BEAR.Sunday CLI Tutorial

Prerequisites

  • PHP 8.2 or higher
  • Composer
  • Git

Step 1: Project Creation

1.1 Create a New Project

VENDOR=MyVendor PACKAGE=Greet composer create-project bear/skeleton greet
cd greet

1.2 Verify Development Server

php -S 127.0.0.1:8080 -t public

Access http://127.0.0.1:8080 in your browser and confirm that “Hello BEAR.Sunday” is displayed.

{
    "greeting": "Hello BEAR.Sunday",
    "_links": {
        "self": {
            "href": "/index"
        }
    }
}

Step 2: Install BEAR.Cli

composer require bear/cli

Step 3: Create Greeting Resource

Create src/Resource/Page/Greeting.php:

<?php

namespace MyVendor\Greet\Resource\Page;

use BEAR\Cli\Attribute\Cli;
use BEAR\Cli\Attribute\Option;
use BEAR\Resource\ResourceObject;

class Greeting extends ResourceObject
{
    #[Cli(
        name: 'greet',
        description: 'Generate a greeting message',
        output: 'message'
    )]
    public function onGet(
        #[Option(shortName: 'n', description: 'Name to greet')]
        string $name = 'World',
        #[Option(shortName: 'l', description: 'Language (en, ja, fr, es)')]
        string $lang = 'en'
    ): static {
        $greeting = match ($lang) {
            'ja' => 'こんにちは',
            'fr' => 'Bonjour',
            'es' => '¡Hola',
            default => 'Hello',
        };
        
        $this->body = [
            'message' => "{$greeting}, {$name}!",
            'language' => $lang
        ];

        return $this;
    }
}

Step 4: Generate CLI Command

Generate the CLI command using your application namespace:

$ vendor/bin/bear-cli-gen 'MyVendor\Greet'
# Generated files:
#   bin/cli/greet         # CLI command
#   var/homebrew/greet.rb # Homebrew formula (if Git repository is configured)

Step 5: Test the CLI Command

5.1 Basic Usage

$ bin/cli/greet --help
Generate a greeting message

Usage: greet [options]

Options:
  --name, -n     Name to greet (default: World)
  --lang, -l     Language (en, ja, fr, es) (default: en)
  --help, -h     Show this help message

$ bin/cli/greet
Hello, World!

$ bin/cli/greet -n "Alice" -l ja
こんにちは, Alice!

5.2 Advanced Examples

# French greeting
$ bin/cli/greet --name "Pierre" --lang fr
Bonjour, Pierre!

# Spanish greeting
$ bin/cli/greet -n "Carlos" -l es
¡Hola, Carlos!

Step 6: Add More Complex Features

6.1 Add Time-Based Greetings

Update the Greeting resource to include time-based greetings:

<?php

namespace MyVendor\Greet\Resource\Page;

use BEAR\Cli\Attribute\Cli;
use BEAR\Cli\Attribute\Option;
use BEAR\Resource\ResourceObject;
use DateTimeImmutable;

class Greeting extends ResourceObject
{
    #[Cli(
        name: 'greet',
        description: 'Generate a time-aware greeting message',
        output: 'message'
    )]
    public function onGet(
        #[Option(shortName: 'n', description: 'Name to greet')]
        string $name = 'World',
        #[Option(shortName: 'l', description: 'Language (en, ja, fr, es)')]
        string $lang = 'en',
        #[Option(shortName: 't', description: 'Include time-based greeting')]
        bool $timeGreeting = false
    ): static {
        $greeting = $this->getGreeting($lang, $timeGreeting);
        
        $this->body = [
            'message' => "{$greeting}, {$name}!",
            'language' => $lang,
            'time' => (new DateTimeImmutable())->format('Y-m-d H:i:s')
        ];

        return $this;
    }
    
    private function getGreeting(string $lang, bool $timeGreeting): string
    {
        $baseGreeting = match ($lang) {
            'ja' => 'こんにちは',
            'fr' => 'Bonjour',
            'es' => '¡Hola',
            default => 'Hello',
        };
        
        if (!$timeGreeting) {
            return $baseGreeting;
        }
        
        $hour = (int) (new DateTimeImmutable())->format('H');
        
        return match ($lang) {
            'ja' => match (true) {
                $hour < 12 => 'おはようございます',
                $hour < 18 => 'こんにちは',
                default => 'こんばんは'
            },
            'fr' => match (true) {
                $hour < 12 => 'Bonjour',
                $hour < 18 => 'Bon après-midi',
                default => 'Bonsoir'
            },
            'es' => match (true) {
                $hour < 12 => 'Buenos días',
                $hour < 18 => 'Buenas tardes',
                default => 'Buenas noches'
            },
            default => match (true) {
                $hour < 12 => 'Good morning',
                $hour < 18 => 'Good afternoon',
                default => 'Good evening'
            }
        };
    }
}

6.2 Test Enhanced Features

# Regenerate CLI command after changes
$ vendor/bin/bear-cli-gen 'MyVendor\Greet'

# Test time-based greetings
$ bin/cli/greet -n "Alice" -l en -t
Good morning, Alice!  # (if run in the morning)

$ bin/cli/greet -n "田中" -l ja -t
おはようございます, 田中!  # (if run in the morning)

Step 7: Testing

7.1 Create Unit Tests

Create tests/Resource/Page/GreetingTest.php:

<?php

namespace MyVendor\Greet\Resource\Page;

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

class GreetingTest extends TestCase
{
    private ResourceInterface $resource;

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

    public function testDefaultGreeting(): void
    {
        $response = $this->resource->get('page://self/greeting');
        
        $this->assertSame(200, $response->code);
        $this->assertSame('Hello, World!', $response->body['message']);
        $this->assertSame('en', $response->body['language']);
    }

    public function testJapaneseGreeting(): void
    {
        $response = $this->resource->get('page://self/greeting', [
            'name' => '太郎',
            'lang' => 'ja'
        ]);
        
        $this->assertSame('こんにちは, 太郎!', $response->body['message']);
        $this->assertSame('ja', $response->body['language']);
    }

    public function testTimeBasedGreeting(): void
    {
        $response = $this->resource->get('page://self/greeting', [
            'name' => 'Alice',
            'lang' => 'en',
            'timeGreeting' => true
        ]);
        
        $this->assertStringContains('Alice!', $response->body['message']);
        $this->assertArrayHasKey('time', $response->body);
    }
}

7.2 Run Tests

$ composer test

Step 8: Deployment and Distribution

8.1 GitHub Repository Setup

If you have a GitHub repository configured, a Homebrew formula will be generated automatically. You can distribute your CLI tool via Homebrew:

# Create a tap repository
$ git clone https://github.com/yourusername/homebrew-tap.git
$ cp var/homebrew/greet.rb homebrew-tap/greet.rb
$ cd homebrew-tap
$ git add greet.rb
$ git commit -m "Add greet formula"
$ git push

8.2 Install via Homebrew

Users can then install your CLI tool:

$ brew tap yourusername/tap
$ brew install greet
$ greet -n "User" -l en
Hello, User!

Conclusion

This tutorial has demonstrated more than just CLI tool creation—it has revealed the essential value of BEAR.Sunday:

The True Value of Resource-Oriented Architecture

One Resource, Multiple Boundaries

  • The Greeting resource functions as Web API, CLI, and Homebrew package with a single implementation
  • No duplication of business logic, maintenance in one place

Boundary-Crossing Framework

BEAR.Sunday functions as a boundary framework, transparently handling:

  • Protocol boundaries: HTTP ↔ Command line
  • Interface boundaries: Web ↔ CLI ↔ Package distribution
  • Environment boundaries: Development ↔ Production ↔ User environments

Design Philosophy in Action

// One resource
class Greeting extends ResourceObject {
    public function onGet(string $name, string $lang = 'en'): static
    {
        // Business logic in one place
    }
}

# As Web API
curl "http://localhost/greeting?name=World&lang=ja"

# As CLI  
./bin/cli/greet -n "World" -l ja

# As Homebrew package
brew install your-vendor/greet && greet -n "World" -l ja

Long-term Maintainability and Productivity

  • DRY Principle: Domain logic is not coupled with interfaces
  • Unified Testing: Testing one resource covers all boundaries
  • Consistent API Design: Same parameter structure for Web API and CLI
  • Future Extensibility: New boundaries (gRPC, GraphQL, etc.) can use the same resource
  • PHP Version Independence: Freedom to continue using what works

Integration with Modern Distribution Systems

BEAR.Sunday resources integrate naturally with modern package systems. By leveraging package managers like Homebrew and the Composer ecosystem, users can utilize tools through unified interfaces without being aware of the execution environment.

BEAR.Sunday’s “Because Everything is a Resource” is not just a slogan, but a design philosophy that realizes consistency and maintainability across boundaries. As experienced in this tutorial, resource-oriented architecture creates boundary-free software and brings new horizons to both development and user experiences.

Next Steps

  • Explore more complex CLI patterns
  • Add configuration file support
  • Implement subcommands
  • Add logging and error handling
  • Create interactive CLI interfaces

For more information, see the CLI documentation and BEAR.Cli repository.


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 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.

php bin/app.php options /todos // console API access (app resource)
php bin/page.php get '/todos?id=1' // console Web access (page resource)
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

Scrutinizer Quality Score codecov Type Coverage Continuous Integration

An aspect oriented framework based on Java AOP Alliance API.

ray/di

Scrutinizer Quality Score codecov Type Coverage Continuous Integration

A Google Guice style DI framework. It contains ray/aop.

bear/resource

Scrutinizer Code Quality codecov Type Coverage Continuous Integration

A REST framework for PHP object as a service. It contains ray/di.

bear/sunday

Scrutinizer Code Quality codecov Type Coverage Continuous Integration

A web application interface package. It contains bear/resource.

bear/package

Scrutinizer Code Quality codecov Type Coverage Continuous Integration

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 Aura.Router v2
Database    
  ray/media-query  
  ray/aura-sql-module Aura.Sql v2
  ray/dbal-module Doctrine DBAL
  ray/cake-database-module CakePHP v3 database
  ray/doctrine-orm-module Doctrine ORM
Storage    
  bear/query-repository CQRS inspired repository
  bear/query-module Separation of external access such as DB or Web API
Web    
  madapaja/twig-module Twig
  ray/web-form-module Web form
  ray/aura-web-module Aura.Web
  ray/aura-session-module Aura.Session
  ray/symfony-session-module Symfony Session
Validation    
  ray/validate-module Aura.Filter
  satomif/extra-aura-filter-module Aura.Filter
Authorization and Authentication    
  ray/oauth-module OAuth
  kuma-guy/jwt-auth-module JSON Web Token
  ray/role-module Zend Acl
  bear/acl-resource ACL based embedded resource
Hypermedia    
  kuma-guy/siren-module Siren
Development    
  ray/test-double Test Double
Asynchronous high performance    
  MyVendor.Swoole Swoole

Vendor Package

You can reuse common packages and tool combinations as modules with only modules and share modules of similar projects.2

Semver

All packages adhere to Semantic Versioning.



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. $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
require dirname(__DIR__) . '/autoload.php';
exit((require dirname(__DIR__) . '/bootstrap.php')('prod-html-app'));

Depending on your context choose a boot file.

// fire php server
php -S 127.0.0.1:8080 public/index.php
// console access
php bin/app.php get /user/1

Context

The composition of the application object $app changes in response to the defined context, so that application behavior changes.

Depending on the defined context the building of the application object $app changes, altering the overall behavior.

For example, WebRouter is bound to RouterInterface by default. However, if Cli mode is set (instead of HTTP) the CliRouter is bound to the RouterInterface and it will then take console input.

There are built-in and custom contexts that can be used in an application.

Built-in Contexts

  • api API Application
  • cli Console Application
  • hal HAL Application
  • prod Production

For app, resources are rendered in JSON. api changes the default resource schema from page to app; web root access (GET /) is from page://self/ to app://self/. Set cli to be a console application. prod` makes it a production application with cache settings, etc.

You can also use a combination of these built-in contexts and add your own custom contexts. If you set the context to prod-hal-api-app your application will run as an API application in production mode using the HAL media type.

Custom Context

Place it in src/Module/ of the application; if it has the same name as the builtin context, the custom context will take precedence. You can override some of the constraints by calling the built-in context from the custom context.

Each application context (cli, app etc) represents a module. For example the cli context relates to a CliModule, then binds all of the DI and AOP bindings that is needed for a console application.

Context Agnostic

The context value is used only to create the root object and then disappears. There is no global “mode” that can be referenced by the application, and the application can not know what context it is currently running in. The behavior should only change through code that is dependent on an interface15 and changes of dependencies by context.



Modules

A Module is a collection of DI & AOP bindings that sets up your application.

BEAR.Sunday doesn’t have a global config file or a config class to set default values for components such as a database or a template engine. Instead for each peice of functionality we set up DI and AOP by injecting configuration values into a stand alone module.

AppModule (src/Module/AppModule.php) is the root module. We use an install() method in here to load each module that we would like to invoke.

You can also override existing bindings by using override().

class AppModule extends AbstractAppModule
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        // ...
        // install additional modules
        $this->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.

// Class binding
$this->bind($interface)->to($class);
// Provider (factory) binding
$this->bind($interface)->toProvider($provider);
// Instance binding
$this->bind($interface)->toInstance($instance);
// Named binding
$this->bind($interface)->annotatedWith($annotation)->to($class);
// Singleton
$this->bind($interface)->to($class)->in(Scope::SINGLETON);
// Constructor binding
$this->bind($interface)->toConstructor($class, $named);

More info can be found at Ray.Di README

Binding Priority

See also: Ray.Di Bindings

Within a Single Module

Bindings declared first take priority. In the following example, Foo1 takes priority:

$this->bind(FooInterface::class)->to(Foo1::class);
$this->bind(FooInterface::class)->to(Foo2::class);

Module Installation Priority

Modules installed first take priority. In the following example, Foo1Module takes priority:

$this->install(new Foo1Module);
$this->install(new Foo2Module);

To give a later module priority, use override(). In the following example, Foo2Module takes priority:

$this->install(new Foo1Module);
$this->override(new Foo2Module);

Context String Priority

Context modules are processed in reverse order (right-to-left). For example, with context prod-hal-api-app:

Installation order: AppModule → ApiModule → HalModule → ProdModule

Later installed modules can override earlier bindings. This means:

  • HalModule takes priority over AppModule
  • ProdModule takes priority over HalModule

When creating a custom context module that needs to override bindings from a built-in context (like HalModule), position it to the left of that context in the context string. For example, to override HalModule’s RenderInterface binding:

// Context: "prod-mycontext-hal-api-app"
// Installation order: AppModule → ApiModule → HalModule → MycontextModule → ProdModule

AOP Bindings

We can “search” for classes and methods with a built-in Matcher, then interceptors can be bound to any found methods.

$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.

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.

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.

Annotations can be obtained using the reflection API.

$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) 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

Ray.Di is the core DI framework used in BEAR.Sunday, which is heavily inspired by Google Guice DI framework.See more detail at Ray.Di Manual.


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.

Interceptor

MethodInterceptors 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.

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.

$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.

With the MethodInvocation object, you can access the target method’s invocation object, method’s and parameters.

Annotations can be obtained using the reflection API.

$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.

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

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 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)

Here are some examples of a resource object:

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;
    }
}
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 orbody) 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.16

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, 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 Idempotent 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.

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 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 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.

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 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.

$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.

$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 for details. Also see the previous resource(v1) document for the previous @Cacheable annotation.

One important REST constraint is resource linking; ResourceObject supports both internal and external linking. See Resource Linking 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.



Resource Parameters

Basics

Web runtime values such as HTTP requests and cookies that ResourceObjects require are passed directly to method arguments. For HTTP requests, the onGet and onPost method arguments receive $_GET and $_POST respectively, according to variable names.

For example, the following $id receives $_GET['id']. When input is from HTTP, string arguments are cast to the specified type.

class Index extends ResourceObject
{
    public function onGet(int $id): static
    {
        // ....

Parameter Types

Scalar Parameters

All parameters passed via HTTP are strings, but specifying non-string types like int will cast them.

Array Parameters

Parameters can be nested data 1. Data sent as JSON or nested query strings can be received as arrays.

class Index extends ResourceObject
{
    public function onPost(array $user): static
    {
        $name = $user['name']; // bear

Class Parameters

Parameters can also be received as dedicated Input classes.

class Index extends ResourceObject
{
    public function onPost(User $user): static
    {
        $name = $user->name; // bear

Input classes are pre-defined with parameters as public properties.

<?php
namespace Vendor\App\Input;

final class User
{
    public int $id;
    public string $name;
}

If a constructor exists, it will be called. 17

<?php
namespace Vendor\App\Input;

final class User
{
    public function __construct(
        public readonly int $id,
        public readonly string $name
    ) {}
}

Namespaces are arbitrary. Input classes can implement methods to aggregate or validate input data.

Ray.InputQuery Integration

Use the #[Input] attribute to leverage type-safe input object generation from the Ray.InputQuery library.

use Ray\InputQuery\Attribute\Input;

class Index extends ResourceObject
{
    public function onPost(#[Input] ArticleInput $article): static
    {
        $this->body = [
            'title' => $article->title,
            'author' => $article->author->name
        ];
        return $this;
    }
}

Parameters with the #[Input] attribute automatically receive structured objects generated from flat query data.

use Ray\InputQuery\Attribute\Input;

final class ArticleInput
{
    public function __construct(
        #[Input] public readonly string $title,
        #[Input] public readonly AuthorInput $author
    ) {}
}

final class AuthorInput  
{
    public function __construct(
        #[Input] public readonly string $name,
        #[Input] public readonly string $email
    ) {}
}

In this case, nested object structures are automatically generated from flat data like title=Hello&authorName=John&authorEmail=john@example.com.

Array data can also be handled.

Simple Arrays

final class TagsInput
{
    public function __construct(
        #[Input] public readonly string $title,
        #[Input] public readonly array $tags
    ) {}
}
class Index extends ResourceObject
{
    public function onPost(#[Input] TagsInput $input): static
    {
        // For tags[]=php&tags[]=web&title=Hello
        // $input->tags = ['php', 'web']
        // $input->title = 'Hello'
    }
}

Object Arrays

Use the item parameter to generate array elements as objects of the specified Input class.

use Ray\InputQuery\Attribute\Input;

final class UserInput
{
    public function __construct(
        #[Input] public readonly string $id,
        #[Input] public readonly string $name
    ) {}
}

class Index extends ResourceObject
{
    public function onPost(
        #[Input(item: UserInput::class)] array $users
    ): static {
        foreach ($users as $user) {
            echo $user->name; // Each element is a UserInput instance
        }
    }
}

This generates arrays from data in the following format:

// users[0][id]=1&users[0][name]=John&users[1][id]=2&users[1][name]=Jane
$data = [
    'users' => [
        ['id' => '1', 'name' => 'John'],
        ['id' => '2', 'name' => 'Jane']
    ]
];
  • When a parameter has the #[Input] attribute: Object generation with Ray.InputQuery
  • When a parameter doesn’t have the #[Input] attribute: Traditional dependency injection

File Upload

Use the #[InputFile] attribute to implement type-safe file upload processing with direct mapping between HTML forms and PHP code. Form name attributes correspond directly to method parameter names, making code the specification and improving readability.

Single File Upload

HTML Form:

<form method="post" enctype="multipart/form-data" action="/image-upload">
    <input type="file" name="image" accept="image/*" required>
    <input type="text" name="title" placeholder="Image title">
    <button type="submit">Upload</button>
</form>

Corresponding resource method:

use Ray\InputQuery\Attribute\InputFile;
use Koriym\FileUpload\FileUpload;
use Koriym\FileUpload\ErrorFileUpload;

class ImageUpload extends ResourceObject
{
    public function onPost(
        #[InputFile(
            maxSize: 1024 * 1024, // 1MB
            allowedTypes: ['image/jpeg', 'image/png', 'image/svg+xml'],
            allowedExtensions: ['jpg', 'jpeg', 'png', 'svg'],
            required: false  // Make file upload optional
        )]
        FileUpload|ErrorFileUpload|null $image = null, // null when no file specified
        string $title = 'Default Title'
    ): static {
        if ($image === null) {
            // Handle case when no file is specified
            $this->body = ['title' => $title, 'image' => null];
            return $this;
        }
        
        if ($image instanceof ErrorFileUpload) {
            // Handle validation errors
            $this->code = 400;
            $this->body = [
                'error' => true,
                'message' => $image->message
            ];
            return $this;
        }

        // Handle successful file upload - move file to destination directory
        $uploadDir = '/var/www/uploads/';
        $originalName = basename($image->name);
        $extension = pathinfo($originalName, PATHINFO_EXTENSION);
        $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '', pathinfo($originalName, PATHINFO_FILENAME));
        $filename = bin2hex(random_bytes(8)) . '_' . uniqid() . '_' . $safeName . '.' . $extension;
        $image->move($uploadDir . $filename);

        $this->body = [
            'success' => true,
            'filename' => $image->name,
            'savedAs' => $filename,
            'size' => $image->size,
            'type' => $image->type,
            'title' => $title
        ];
        return $this;
    }
}

Multiple File Upload

HTML Form:

<form method="post" enctype="multipart/form-data" action="/gallery-upload">
    <input type="file" name="images[]" multiple accept="image/*" required>
    <input type="text" name="galleryName" placeholder="Gallery name">
    <button type="submit">Upload</button>
</form>

Corresponding resource method:

class GalleryUpload extends ResourceObject
{
    /**
     * @param array<FileUpload|ErrorFileUpload> $images
     */
    public function onPost(
        #[InputFile(
            maxSize: 2 * 1024 * 1024, // 2MB
            allowedTypes: ['image/jpeg', 'image/png', 'image/svg+xml']
        )]
        array $images, // Receive multiple files as array
        string $galleryName = 'Default Gallery'
    ): static {
        $uploadDir = '/var/www/uploads/gallery/';
        $results = [];
        $hasError = false;

        foreach ($images as $index => $image) {
            if ($image instanceof ErrorFileUpload) {
                $hasError = true;
                $results[] = [
                    'index' => $index,
                    'error' => true,
                    'message' => $image->message
                ];
                continue;
            }

            // Save file
            $originalName = basename($image->name);
            $extension = pathinfo($originalName, PATHINFO_EXTENSION);
            $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '', pathinfo($originalName, PATHINFO_FILENAME));
            $filename = bin2hex(random_bytes(8)) . '_' . uniqid() . '_' . $safeName . '.' . $extension;
            $image->move($uploadDir . $filename);

            $results[] = [
                'index' => $index,
                'success' => true,
                'filename' => $image->name,
                'savedAs' => $filename,
                'size' => $image->size,
                'type' => $image->type
            ];
        }

        $this->code = $hasError ? 207 : 200; // 207 Multi-Status
        $this->body = [
            'galleryName' => $galleryName,
            'files' => $results,
            'total' => count($images),
            'hasErrors' => $hasError
        ];
        return $this;
    }
}

Testing File Uploads

File upload functionality can be easily tested:

use Koriym\FileUpload\FileUpload;
use Koriym\FileUpload\ErrorFileUpload;

class FileUploadTest extends TestCase
{
    public function testSuccessfulFileUpload(): void
    {
        // Create FileUpload object from actual file
        $fileUpload = FileUpload::fromFile(__DIR__ . '/fixtures/test.jpg');
        
        $resource = $this->getResource();
        $result = $resource->post('app://self/image-upload', [
            'image' => $fileUpload,
            'title' => 'Test Image'
        ]);
        
        $this->assertSame(200, $result->code);
        $this->assertTrue($result->body['success']);
        $this->assertSame('test.jpg', $result->body['filename']);
    }
    
    public function testFileUploadValidationError(): void
    {
        // Simulate validation error
        $errorFileUpload = new ErrorFileUpload([
            'name' => 'large.jpg',
            'type' => 'image/jpeg',
            'size' => 5 * 1024 * 1024, // 5MB - exceeds size limit
            'tmp_name' => '/tmp/test',
            'error' => UPLOAD_ERR_OK
        ], 'File size exceeds maximum allowed size');
        
        $resource = $this->getResource();
        $result = $resource->post('app://self/image-upload', [
            'image' => $errorFileUpload
        ]);
        
        $this->assertSame(400, $result->code);
        $this->assertTrue($result->body['error']);
        $this->assertStringContainsString('exceeds maximum allowed size', $result->body['message']);
    }
    
    public function testMultipleFileUpload(): void
    {
        // Test multiple files
        $file1 = FileUpload::fromFile(__DIR__ . '/fixtures/image1.jpg');
        $file2 = FileUpload::fromFile(__DIR__ . '/fixtures/image2.png');
        
        $resource = $this->getResource();
        $result = $resource->post('app://self/gallery-upload', [
            'images' => [$file1, $file2],
            'galleryName' => 'Test Gallery'
        ]);
        
        $this->assertSame(200, $result->code);
        $this->assertSame(2, $result->body['total']);
        $this->assertCount(2, $result->body['files']);
    }
}

The #[InputFile] attribute enables direct correspondence between HTML form input elements and PHP method parameters, achieving type-safe and intuitive file upload processing. Array support makes multiple file uploads easy to implement, and testing is also straightforward.

For more details, see the Ray.InputQuery documentation.

Enum Parameters

You can specify PHP8.1 enumerations to restrict possible values.

enum IceCreamId: int
{
    case VANILLA = 1;
    case PISTACHIO = 2;
}
class Index extends ResourceObject
{
    public function onGet(IceCreamId $iceCreamId): static
    {
        $id = $iceCreamId->value; // 1 or 2
    }
}

In the above case, passing anything other than 1 or 2 will raise a ParameterInvalidEnumException.

Web Context Binding

Values from PHP superglobals like $_GET and $_COOKIE can be bound to method arguments instead of retrieving them within methods.

use Ray\WebContextParam\Annotation\QueryParam;

class News extends ResourceObject
{
    public function foo(
        #[QueryParam('id')] string $id
    ): static {
        // $id = $_GET['id'];

You can also bind values from $_ENV, $_POST, and $_SERVER.

use Ray\WebContextParam\Annotation\QueryParam;
use Ray\WebContextParam\Annotation\CookieParam;
use Ray\WebContextParam\Annotation\EnvParam;
use Ray\WebContextParam\Annotation\FormParam;
use Ray\WebContextParam\Annotation\ServerParam;

class News extends ResourceObject
{
    public function onGet(
        #[QueryParam('id')] string $userId,            // $_GET['id']
        #[CookieParam('id')] string $tokenId = "0000", // $_COOKIE['id'] or "0000" when unset
        #[EnvParam('app_mode')] string $app_mode,      // $_ENV['app_mode']
        #[FormParam('token')] string $token,           // $_POST['token']
        #[ServerParam('SERVER_NAME')] string $server   // $_SERVER['SERVER_NAME']
    ): static {

When clients specify values, those values take precedence and bound values become invalid. This is useful for testing.

Resource Binding

The #[ResourceParam] annotation can bind results from other resource requests to method arguments.

use BEAR\Resource\Annotation\ResourceParam;

class News extends ResourceObject
{
    public function onGet(
        #[ResourceParam('app://self//login#nickname')] string $name
    ): static {

In this example, when the method is called, it makes a get request to the login resource and receives $body['nickname'] as $name.

Content Negotiation

HTTP request content-type headers are supported. application/json and x-www-form-urlencoded media types are distinguished and values are passed to parameters. 18


Reousrce link

Resources can be linked to other resources. There are two types of links: external links 19, which link external resources, and internal links 20, which embed other resources in the resource itself.

Specify links by rel (relation) and href of the link name. The href can be a regular URI or RFC6570 URI template.

    #[Link rel: 'profile', href: '/profile{?id}']
    public function onGet($id): static
    {
        $this->body = [
            'id' => 10
        ];

        return $this;
    }

In the above example, href is represented by and $body['id'] is assigned to {?id}. The output in HAL format is as follows

{
    "id": 10,
    "_links": {
        "self": {
            "href": "/test"
        },
        "profile": {
            "href": "/profile?id=10"
        }
    }
}

A resource can embed another resource. Specify the resource in the src of #[Embed].

Internally linked resources may also internally link other resources. In that case, another internally linked resource is needed, and the process is repeated recursively to obtain a resource graph. The client can retrieve the desired set of resources at once without having to fetch the resources multiple times. 21 For example, instead of calling a customer resource and a product resource respectively, embed them both in an order resource.

use BEAR\Resource\Annotation\Embed;

class News extends ResourceObject
{
    #[Embed(rel: 'sports', src: '/news/sports')]
    #[Embed(rel: 'weather', src: '/news/weather')]
    public function onGet(): static

It is the resource request that is embedded. It is executed at rendering time, but before that you can add arguments with the addQuery() method or replace them with withQuery().

A URI template can be used for the src, and request method arguments will be bound to it. (Unlike external links, it is not $body)

use BEAR\Resource\Annotation\Embed;

class News extends ResourceObject
{
    #[Embed(rel: 'website', src: '/website{?id}']
    public function onGet(string $id): static
    {
        // ...
        $this->body['website']->addQuery(['title' => $title]); // 引数追加

Self linking

Linking a relation as _self in #[Embed] copies the linked resource state to its own resource state.

namespace MyVendor\Weekday\ResourcePage;.

class Weekday extends ResourceObject
{
#[Embed(rel: '_self', src: 'app://self/weekday{?year,month,day}'])
public function onGet(string $id): static
{

In this example, the Page resource copies the state of the weekday resource of the App resource to itself.

Handled as _embedded in the HAL renderer.

Clients can link resources connected by hyperlinks.

$blog = $this
    ->resource
    ->get
    ->uri('app://self/user')
    ->withQuery(['id' => 1])
    ->linkSelf("blog")
    ->eager
    ->request()
    ->body;

There are three types of links. The body linked resource of the original resource is embedded using $rel as the key.

  • linkSelf($rel) which will be replaced with the link destination.
  • linkNew($rel) the linked resource is added to the original resource
  • linkCrawl($rel) crawl the link and create a resource graph.

crawl

Crawls are lists (arrays) of resources, and links can be traversed in sequence to compose complex resource graphs. Just as a crawler crawls a web page, the resource client crawls hyperlinks and generates a source graph.

Crawl Example

Consider a resource graph with author, post, meta, tag, and tag/name associated with each. Name this resource graph post-tree and specify a hyperreference href in the `#[Link]’ attribute of each resource.

The first starting point, the author resource, has a hyperlink to the post resource. 1:n relationship.

#[Link(crawl: "post-tree", rel: "post", href: "app://self/post?author_id={id}")]
public function onGet($id = null)

The post resource has hyperlinks to the meta and tag resources. 1:n relationship.

#[Link(crawl: "post-tree", rel: "meta", href: "app://self/meta?post_id={id}")]
#[Link(crawl: "post-tree", rel: "tag", href: "app://self/tag?post_id={id}")]
public function onGet($author_id)
{

A tag resource is just an ID with a hyperlink to the corresponding tag/name resource. 1:1 relationship.

#[Link(crawl:"post-tree", rel:"tag_name", href:"app://self/tag/name?tag_id={tag_id}")]
public function onGet($post_id)

Each is now connected. Request with a crawl name.

$graph = $resource
  ->get
  ->uri('app://self/marshal/author')
  ->linkCrawl('post-tree')
  ->eager
  ->request();

When a resource client finds a crawl name specified in the #[Link] attribute, it creates a resource graph by connecting resources by their rel names.

var_export($graph->body);

array (
    0 =>
    array (
        'name' => 'Athos',
        'post' =>
        array (
            0 =>
            array (
                'author_id' => '1',
                'body' => 'Anna post #1',
                'meta' =>
                array (
                    0 =>
                    array (
                        'data' => 'meta 1',
                    ),
                ),
                'tag' =>
                array (
                    0 =>
                    array (
                        'tag_name' =>
                        array (
                            0 =>
                            array (
                                'name' => 'zim',
                            ),
                        ),
                    ),
 ...

DataLoader Beta

Available in bear/resource:1.x-dev

When crawling resources with links, each child resource triggers individual queries, causing the N+1 problem. DataLoader solves this by batching multiple resource requests into a single efficient query.

The N+1 Problem

Request: GET /author/1 with linkCrawl('post-tree')

[Query 1] SELECT * FROM author WHERE id = 1
  └─ Author has 3 posts

[Query 2] SELECT * FROM post WHERE author_id = 1
  └─ Returns 3 posts (id: 10, 11, 12)

[Query 3] SELECT * FROM meta WHERE post_id = 10  ← N+1 starts here
[Query 4] SELECT * FROM meta WHERE post_id = 11
[Query 5] SELECT * FROM meta WHERE post_id = 12

Total: 5 queries (grows with data size!)

With DataLoader

[Query 1] SELECT * FROM author WHERE id = 1
[Query 2] SELECT * FROM post WHERE author_id = 1
[Query 3] SELECT * FROM meta WHERE post_id IN (10, 11, 12)  ← Batched!

Total: 3 queries (constant regardless of data size)

Usage

Add the dataLoader parameter to the #[Link] attribute:

#[Link(crawl: 'post-tree', rel: 'meta', href: 'app://self/meta{?post_id}', dataLoader: MetaDataLoader::class)]
public function onGet($author_id)
{

DataLoader Implementation

Implement DataLoaderInterface to batch queries:

use Aura\Sql\ExtendedPdoInterface;
use BEAR\Resource\DataLoader\DataLoaderInterface;

class MetaDataLoader implements DataLoaderInterface
{
    public function __construct(
        private ExtendedPdoInterface $pdo
    ){}

    /**
     * @param list<array<string, mixed>> $queries
     * @return list<array<string, mixed>>
     */
    public function __invoke(array $queries): array
    {
        $postIds = array_column($queries, 'post_id');

        // Batch query: SELECT * FROM meta WHERE post_id IN (...)
        return $this->pdo->fetchAll(
            'SELECT * FROM meta WHERE post_id IN (:post_ids)',
            ['post_ids' => $postIds]
        );
    }
}

This example uses SQL directly for clarity, but Ray.MediaQuery can also be used for the implementation.

Key Inference

The key for matching results is auto-inferred from the URI template:

URI Template Inferred Key
{?post_id} post_id
post_id={id} post_id
{?post_id,locale} post_id, locale

The returned rows must contain the key column(s) for proper distribution.

Multiple Keys

For multiple key parameters, use all keys in your query:

// URI template: app://self/translation{?post_id,locale}
// $queries: [['post_id' => '1', 'locale' => 'en'], ['post_id' => '1', 'locale' => 'ja']]

public function __invoke(array $queries): array
{
    // Build query using both keys
    $sql = "SELECT * FROM translation WHERE (post_id, locale) IN (...)";
    // ...
}

Rendering and transfer

The request method of a ResourceObject is not concerned with the representation of the resource. The context-sensitive injected renderer generates the representation of the resource. The same application can be output in HTML or JSON and benefit by simply changing the context.

Lazy evaluation

Rendering occurs when the resource is string-evaluated.


$weekday = $api->resource->get('app://self/weekday', ['year' => 2000, 'month'=> 1, 'day'=> 1]);
var_dump($weekday->body);
//array(1) {
//    ["weekday"]=>
//  string(3) "Sat"
//}

echo $weekday;
//{
//    "weekday": "Sat",
//    "_links": {
//    "self": {
//        "href": "/weekday/2000/1/1"
//        }
//    }
//}

Renderer

Each ResourceObject is injected with a renderer for its representation as specified by its context. When performing resource-specific rendering, inject or set the renderer property.

Example: If you write a renderer for the default JSON representation from scratch

class Index extends ResourceObject
{
    #[Inject]
    public function setRenderer(RenderInterface $renderer)
    {
        $this->renderer = new class implements RenderInterface {
            public function render(ResourceObject $ro)
            {
                $ro->headers['content-type'] = 'application/json;';
                $ro->view = json_encode($ro->body);

                return $ro->view;
            }
        };
    }
}

Transfer

Transfers the resource representation injected into the root object $app to the client (console or web client). Normally, output is done with the header function or echo, but for large data, etc., stream transfer is useful.

Override the transfer method to perform resource-specific transfers.

public function transfer(TransferInterface $responder, array $server)
{
    $responder($this, $server);
}

Resource autonomy

Each resource class has the ability to change its own resource state upon request and transfer it as an expression.


Router

The router converts resource requests for external contexts such as Web and console into resource requests inside BEAR.Sunday.

$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.

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”.

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&param2=two
  • application/json // {“param1”: “one”, “param2”: “one”}

Please also see the PUT method support 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.

composer require bear/aura-router-module ^2.0

Install AuraRouterModule with the path of the router script.

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
/* @var \Aura\Router\Map $map */
$map->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.

$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)

$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.

$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.

Generating Paths From Routes

You can generate a URI from the name of the route and the value of the parameter.

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.

$this->install(new RequestHeaderModule());

Custom Router Component

Implement RouterInterface with by referring to BEAR.AuraRouterModule.


*This document 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 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 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
namespace MyVendor\Todo\Module;

use BEAR\Package\Context\ProdModule as PackageProdModule;
use BEAR\QueryRepository\CacheVersionModule;
use BEAR\Resource\Module\OptionsMethodModule;
use BEAR\Package\AbstractAppModule;

class ProdModule extends AbstractModule
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->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.

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 and Redis cache engines are provided.

Memcached

<?php
namespace BEAR\HelloWorld\Module;

use BEAR\QueryRepository\StorageMemcachedModule;
use BEAR\Resource\Module\ProdLoggerModule;
use BEAR\Package\Context\ProdModule as PackageProdModule;
use BEAR\Package\AbstractAppModule;
use Ray\Di\Scope;

class ProdModule extends AbstractModule
{
    protected function configure()
    {
        // memcache
        // {host}:{port}:{weight},...
        $memcachedServers = 'mem1.domain.com:11211:33,mem2.domain.com:11211:67';
        $this->install(new StorageMemcachedModule($memcachedServers));

        // Install Prod logger
        $this->install(new ProdLoggerModule);
        // Install default ProdModule
        $this->install(new PackageProdModule);
    }
}

Redis

// 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.

// 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.

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 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 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 of Deployer.

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.

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 and opcache.preload 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 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
class AuthProvider 
{  // Only for instantiation, so implementation is not required
}

.compile.php

<?php
require __DIR__ . '/tests/Null/AuthProvider.php'; // Always-generatable Null object
$_SERVER[__REQUIRED_KEY__] = 'fake'; // For cases where errors occur without specific environment variables

This allows you to avoid exceptions and perform compilation. Additionally, since Symfony’s cache component connects to the cache engine in the constructor, it’s good to load a dummy adapter during compilation like this:

tests/Null/RedisAdapter.php

namespace Ray\PsrCacheModule;

use Ray\Di\ProviderInterface;
use Serializable;
use Symfony\Component\Cache\Adapter\RedisAdapter as OriginAdapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;

class RedisAdapter extends OriginAdapter implements Serializable
{
    use SerializableTrait;
    
    public function __construct(ProviderInterface $redisProvider, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
    {
      // do nothing
    }
}

module.dot

When you compile, a “dot file” is output, so you can convert it to an image file with graphviz or use GraphvizOnline to display the object graph. Also, please see the object graph of the skeleton.

dot -T svn module.dot > module.svg

Bootstrap Performance Tuning

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 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 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

----

***

# Import

BEAR applications can cooperate with multiple BEAR applications into a single system without having to be microservices. It is also easy to use BEAR resources from other applications.

## Composer Install

Install the BEAR application you want to use as a composer package.

composer.json
```json
{
  "require": {
    "bear/package": "^1.13",
    "my-vendor/weekday": "dev-master"
  },
  "repositories": [
    {
      "type": "vcs",
      "url": "https://github.com/bearsunday/tutorial1.git"
    }
  ]
}
```

Requires `bear/package ^1.13`.

## Module Install

Install other applications with `ImportAppModule`, specifying the hostname, application name (namespace) and context to import.

```diff
+use BEAR\Package\Module\ImportAppModule;
+use BEAR\Package\Module\Import\ImportApp;

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        // ...
+        $this->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.


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.

Multilingual Framework

Using BEAR.Thrift, you can access resources from other languages, different versions of PHP, or BEAR applications using Apache Thrift. Apache Thrift is a framework that enables efficient communication between different languages.


Database

The following modules are available for database use, with different problem solving methods. They are all independent libraries for SQL based on PDO.

Having static SQL in a file22 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.

Module

Modules are provided for using the database. They are all independent libraries for SQL.

Ray.AuraSqlModule is a PDO extension Aura.Sql and a query builder Aura.SqlQuery 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 23 .

Other

DBAL is Doctrine and CakeDB is CakePHP’s DB library. Ray.QueryModule is an earlier library of Ray.MediaQuery that converts SQL to anonymous functions.

CQRS Read Model

BEAR.Projection provides SQL-based projections mapped to typed value objects. Projections are exposed as resources via the query:// scheme and can be embedded with #[Embed] for parallel execution.

#[Embed(rel: 'profile', src: 'query://self/user_profile{?id}')]
#[Embed(rel: 'orders', src: 'query://self/user_orders{?id}')]
public function onGet(string $id): static


Ray.MediaQuery

Ray.MediaQuery generates and injects query execution objects from database query interfaces.

  • Clarifies the boundary between domain layer and infrastructure layer.
  • Reduces boilerplate code.
  • Since it’s independent of the actual external media, storage can be changed later. Enables easy parallel development and stub creation.

Installation

composer require ray/media-query

Note: For the same interface-driven approach over Web APIs, see ray/web-query.

Usage

Define an interface for database access.

Interface Definition

Specify the SQL ID with the #[DbQuery] attribute.

use Ray\MediaQuery\Annotation\DbQuery;

interface TodoAddInterface
{
    #[DbQuery('todo_add')]
    public function add(string $title): void;
}

Module Configuration

Specify SQL directory and interface directory with MediaQuerySqlModule.

use Ray\AuraSqlModule\AuraSqlModule;
use Ray\MediaQuery\MediaQuerySqlModule;

protected function configure(): void
{
    $this->install(
        new MediaQuerySqlModule(
            interfaceDir: '/path/to/query/interfaces',
            sqlDir: '/path/to/sql'
        )
    );
    $this->install(new AuraSqlModule(
        'mysql:host=localhost;dbname=test',
        'username',
        'password'
    ));
}

MediaQuerySqlModule requires AuraSqlModule to be installed.

Injection

Objects are generated directly from interfaces and injected. No implementation class coding is required.

class Todo
{
    public function __construct(
        private TodoAddInterface $todoAdd
    ) {}

    public function add(string $title): void
    {
        $this->todoAdd->add($title);
    }
}

DbQuery

SQL execution is mapped to methods, binding the SQL specified by ID with method arguments for execution. For example, with ID todo_item, it executes todo_item.sql SQL statement bound with ['id' => $id].

  • Prepare SQL files in the $sqlDir directory.
  • SQL files can contain multiple SQL statements. The last SELECT statement becomes the return value.

The two basic shapes are Entity (single row hydrated to an object) and Entity list (rowlist hydrated to an array of objects). Other shapes — raw assoc arrays, custom collections, pagination, DML results — build on these.

Entity (single row)

When you specify an entity class as the return type, the SQL result is automatically converted (hydrated) into an instance of that class.

interface TodoItemInterface
{
    #[DbQuery('todo_item')]
    public function getItem(string $id): Todo;
}

Using constructor property promotion creates type-safe and immutable entities.

final class Todo
{
    public function __construct(
        public readonly string $id,
        public readonly string $title
    ) {}
}

Values are bound to constructor arguments by position, in the order of the columns in the SELECT clause. Column names (e.g. user_name) do not need to match property names (e.g. $userName) — just make sure the column order matches the constructor argument order.

Entity|null is also supported and returns null when no row matches.

Note: When the entity has no constructor, Ray.MediaQuery falls back to PDO’s FETCH_CLASS and maps column name → property name (no snake_case conversion). This avoids any dependency on column ordering, which is useful for wide read-only DTOs or PHP 8.4 readonly class declarations.

Entity list (rowlist)

Declare array as the return type to receive multiple rows. Tell the framework which entity to hydrate each row into via a @return list<Entity> docblock or the factory: parameter on #[DbQuery].

interface TodoListInterface
{
    /** @return list<Todo> */
    #[DbQuery('todo_list')]
    public function list(): array;

    #[DbQuery('todo_list', factory: TodoFactory::class)]
    public function listByFactory(): array;
}

Without @return list<Entity> or factory:, rows are returned as associative arrays — see type: ‘row’ below for the single-row equivalent.

type: ‘row’ (raw associative array)

By default, an array return type yields a rowlist ([['id' => '1', 'title' => 'run'], ...]). To receive a single row — for example an aggregate result such as ['total' => 10, 'active' => 5]directly as an associative array, specify type: 'row'. Without it, that row is returned at $result[0].

interface TodoItemInterface
{
    #[DbQuery('todo_stats', type: 'row')]
    public function getStats(string $id): array;  // ['total' => 10, 'active' => 5]
}

AffectedRows (UPDATE / DELETE row count)

Declare AffectedRows as the return type to receive the affected row count of an UPDATE / DELETE as a typed value rather than a bare int.

use Ray\MediaQuery\Result\AffectedRows;

interface TodoRepositoryInterface
{
    #[DbQuery('todo_delete')]
    public function delete(string $id): AffectedRows;
}

$affected = $todoRepo->delete($id);
$affected->count;        // int — number of affected rows
$affected->isAffected(); // bool — true when count > 0

When a SQL file contains multiple statements, AffectedRows reflects the last executed statement only.

Executable examples: TodoAffectedInterface and DbQueryAffectedRowsTest.

InsertedRow (INSERT resolved values and id)

Use InsertedRow to recover the values the framework injected on the caller’s behalf (UUIDs, timestamps, DateTime → SQL strings, ToScalarInterface reductions) together with the auto-increment id reported by the driver. The same SQL id can be reused with a different return type — the framework switches behaviour (execute-only / affected count / inserted id) based on the declared return type alone.

use Ray\MediaQuery\Result\InsertedRow;

interface TodoAddInterface
{
    #[DbQuery('todo_add')]
    public function add(string $title): void;

    #[DbQuery('todo_add')]
    public function addReturning(string $title): InsertedRow;
}

$inserted = $todoAdd->addReturning('Write docs');
$inserted->values;  // array<string, mixed> — resolved values bound to the driver
$inserted->id;      // ?string — auto-increment id, null when none was assigned

$inserted->id is normalised to null when the driver returns false / '' / '0'.

PostQueryInterface (custom typed results)

Sometimes you want to wrap a SELECT result in your own collection type — exposing domain methods like published() / titles() instead of returning a plain array<Article>. Declare a class that implements PostQueryInterface as the return type, and the framework collects the post-execution state into a PostQueryContext, passes it to the static fromContext() factory, and lets the class decide how to assemble itself.

interface PostQueryInterface
{
    public static function fromContext(PostQueryContext $context): static;
}

PostQueryContext provides four readonly properties:

Property Type Purpose
$statement PDOStatement The executed statement; inspect rowCount(), column metadata, etc.
$pdo ExtendedPdoInterface The connection; useful for lastInsertId() and follow-up reads.
$values array<string, mixed> Parameter values resolved by ParamConverter / ParamInjector (UUIDs, timestamps, value object scalars).
$rows array<mixed> Fetched rows on SELECT — hydrated entities when @return Wrapper<Entity> or factory: resolves an entity, associative arrays otherwise. Always [] for DML.
use Ray\MediaQuery\Result\PostQueryContext;
use Ray\MediaQuery\Result\PostQueryInterface;

/** @implements IteratorAggregate<int, Article> */
final class Articles implements PostQueryInterface, IteratorAggregate, Countable
{
    /** @param list<Article> $rows */
    public function __construct(public readonly array $rows) {}

    public static function fromContext(PostQueryContext $context): static
    {
        /** @var list<Article> $rows */
        $rows = $context->rows;
        return new static($rows);
    }

    public function getIterator(): ArrayIterator { return new ArrayIterator($this->rows); }
    public function count(): int { return count($this->rows); }
}

interface ArticleRepositoryInterface
{
    #[DbQuery('article_list', factory: ArticleFactory::class)]
    public function list(): Articles;
}

Hydration of each row is configured the same way as for an Entity list: via a generic @return YourWrapper<Entity> docblock or factory:. The wrapper uses composition rather than inheritance, so it can hold any internal collection — Laravel Collection, Doctrine ArrayCollection, or a custom one — without coupling to any specific library.

Executable examples in Ray.MediaQuery:

  • Articles — collection wrapper around PostQueryContext::$rows
  • ArticlesInterface — declaring assoc rows, docblock-hydrated rows, and factory: hydrated rows

AffectedRows and InsertedRow are themselves implementations of PostQueryInterface. If you need a custom DML result type — e.g. one that bundles audit logging or aggregate counters — you can build it through the same mechanism.

Return type cheat sheet

  Single row Rowlist
Hydrated Entity / Entity|null array + @return list<Entity> or factory:
Assoc array array + #[DbQuery(type: 'row')] array (no docblock / factory:)

For richer return types:

  • MyColl implementing PostQueryInterface — custom typed collection wrappers
  • PagesInterface + #[Pager] — pagination
  • AffectedRows — DML affected row count
  • InsertedRow — DML insert id + resolved values
  • void — DML execute only

Parameters

DateTime

You can pass value objects as parameters. For example, DateTimeInterface objects can be specified like this:

interface TaskAddInterface
{
    #[DbQuery('task_add')]
    public function __invoke(string $title, DateTimeInterface $createdAt = null): void;
}

Values are converted to date-formatted strings during SQL execution.

INSERT INTO task (title, created_at) VALUES (:title, :createdAt); # 2021-2-14 00:00:00

If no value is passed, the bound current time is injected. This eliminates the need to hard-code NOW() in SQL or pass current time every time.

Test Time

For testing, you can bind DateTimeInterface to a single time like this:

$this->bind(DateTimeInterface::class)->to(UnixEpochTime::class);

Value Objects (VO)

When value objects other than DateTime are passed, the return value of the toScalar() method implementing ToScalarInterface, or the __toString() method becomes the argument.

interface MemoAddInterface
{
    #[DbQuery('memo_add')]
    public function __invoke(string $memo, UserId $userId = null): void;
}
class UserId implements ToScalarInterface
{
    public function __construct(
        private readonly LoginUser $user
    ) {}

    public function toScalar(): int
    {
        return $this->user->id;
    }
}
INSERT INTO memo (user_id, memo) VALUES (:user_id, :memo);

Parameter Injection

Note that the default value null for value object arguments is never used in SQL or Web requests. When no value is passed, the scalar value of the value object injected by parameter type is used instead of null.

public function __invoke(Uuid $uuid = null): void; // UUID is generated and passed

Pagination

You can paginate SELECT queries with the #[Pager] attribute.

use Ray\MediaQuery\Annotation\DbQuery;
use Ray\MediaQuery\Annotation\Pager;
use Ray\MediaQuery\PagesInterface;

interface TodoList
{
    #[DbQuery('todo_list'), Pager(perPage: 10, template: '/{?page}')]
    public function __invoke(): PagesInterface;
}

You can get the count with count(), and get page objects with array access by page number. PagesInterface is a SQL lazy execution object.

$pages = ($todoList)();
$cnt = count($pages);    // Count SQL is generated and queried when count() is called
$page = $pages[2];       // DB query for that page is executed when array access is made

// $page->data           // sliced data
// $page->current;       // current page number
// $page->total          // total count
// $page->hasNext        // whether next page exists
// $page->hasPrevious    // whether previous page exists
// $page->maxPerPage;    // maximum items per page
// (string) $page        // pager HTML

SqlQuery

SqlQuery executes SQL by specifying the SQL file ID. Used when preparing implementation classes for detailed implementation.

class TodoItem implements TodoItemInterface
{
    public function __construct(
        private SqlQueryInterface $sqlQuery
    ) {}

    public function __invoke(string $id): array
    {
        return $this->sqlQuery->getRow('todo_item', ['id' => $id]);
    }
}

get* Methods

Use appropriate get* methods to retrieve SELECT results based on the expected result type.

$sqlQuery->getRow($queryId, $params);        // Result is single row
$sqlQuery->getRowList($queryId, $params);    // Result is multiple rows
$statement = $sqlQuery->getStatement();       // Get PDO Statement
$pages = $sqlQuery->getPages();              // Get pager

Ray.MediaQuery includes Ray.AuraSqlModule. For lower-level operations, use Aura.Sql’s Query Builder or PDO-extended Aura.Sql. doctrine/dbal is also available.

Like parameter injection, passing DateTimeInterface objects converts them to date-formatted strings.

$sqlQuery->exec('memo_add', [
    'memo' => 'run',
    'created_at' => new DateTime()
]);

Other objects are converted to toScalar() or __toString() values.

Integration with Ray.InputQuery

When using Ray.InputQuery with BEAR.Resource, Input classes can be passed directly as MediaQuery parameters.

use Ray\InputQuery\Attribute\Input;

final class UserCreateInput
{
    public function __construct(
        #[Input] public readonly string $name,
        #[Input] public readonly string $email,
        #[Input] public readonly int $age
    ) {}
}
interface UserCreateInterface
{
    #[DbQuery('user_create')]
    public function add(UserCreateInput $input): void;
}

Input object properties are automatically expanded to SQL parameters.

-- user_create.sql
INSERT INTO users (name, email, age) VALUES (:name, :email, :age);

This integration enables consistent type-safe data flow from ResourceObject to MediaQuery.

Profiler

Media access is logged by loggers. By default, a memory logger for testing is bound.

public function testAdd(): void
{
    $this->sqlQuery->exec('todo_add', $todoRun);
    $this->assertStringContainsString(
        'query: todo_add({"id":"1","title":"run"})',
        (string) $this->log
    );
}

You can implement your own MediaQueryLoggerInterface to benchmark each media query or log with injected PSR loggers.

PerformSqlInterface

By implementing PerformSqlInterface, you can completely customize the SQL execution layer. Replace the default execution process with your own implementation to achieve advanced logging, performance monitoring, security controls, and more.

use Exception;
use Ray\MediaQuery\PerformSqlInterface;

final class CustomPerformSql implements PerformSqlInterface
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    #[Override]
    public function perform(ExtendedPdoInterface $pdo, string $sqlId, string $sql, array $values): PDOStatement
    {
        $startTime = microtime(true);

        // Custom logging
        $this->logger->info("Executing SQL: {$sqlId}", [
            'sql' => $sql,
            'params' => $values
        ]);

        try {
            /** @var array<string, mixed> $values */
            $statement = $pdo->perform($sql, $values);

            // Execution time logging
            $executionTime = microtime(true) - $startTime;
            $this->logger->info("SQL executed successfully", [
                'sqlId' => $sqlId,
                'execution_time' => $executionTime
            ]);

            return $statement;
        } catch (Exception $e) {
            $this->logger->error("SQL execution failed: {$sqlId}", [
                'error' => $e->getMessage(),
                'sql' => $sql
            ]);
            throw $e;
        }
    }
}

To use your custom implementation, bind it in the DI container:

use Ray\MediaQuery\PerformSqlInterface;

protected function configure(): void
{
    $this->bind(PerformSqlInterface::class)->to(CustomPerformSql::class);
}

SQL Template

You can customize SQL log formatting to include query IDs in the executed SQL, making it easier to identify which queries are running when analyzing slow logs.

Use MediaQuerySqlTemplateModule to customize the SQL log format.

use Ray\MediaQuery\MediaQuerySqlTemplateModule;

protected function configure(): void
{
    $this->install(new MediaQuerySqlTemplateModule("-- App: .sql\n"));
}

Available template variables:

  • {{ id }}: Query ID
  • {{ sql }}: The actual SQL statement

Default template: -- {{ id }}.sql\n{{ sql }}

This feature includes the query ID as a comment in the executed SQL, making it easy to identify which application query was executed when analyzing database slow logs.

-- App: todo_item.sql
SELECT * FROM todo WHERE id = :id

Validation

  • You can define resource APIs in the JSON schema.
  • You can separate the validation code with #[Valid], #[OnValidate] attribute.
  • Please see the form for validation by web form.

JSON Schema

The JSON Schema 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

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

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


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

{
  "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


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


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

{
  "$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'.

#[JsonSchema(schema: 'user.json', target: 'view')]

#[Valid] attribute

The #[Valid] attribute 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, Respect\Validation, and PHP Standard Filter

Install

Install Ray.ValidateModule via composer.

composer require ray/validate-module

Installing ValidateModule in your application module src/Module/AppModule.php.

use Ray\Validation\ValidateModule;

class AppModule extends AbstractAppModule
{
    protected function configure()
    {
        // ...
        $this->install(new ValidateModule);
    }
}

Attribute

There are three attributes #[Valid], #[OnValidate], #[OnFailure] for validation.

Annotate the method that you want to validate with #[Valid]

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.

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

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

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.


Command Line Interface (CLI)

BEAR.Sunday’s Resource Oriented Architecture (ROA) represents all application functionality as URI-addressable resources. This approach allows resources to be accessed through various means, not just through the web.

$ php bin/page.php '/greeting?name=World&lang=fr'
{
    "greeting": "Bonjour, World",
    "lang": "fr"
}

BEAR.Cli is a tool that converts these resources into native CLI commands and makes them distributable via Homebrew, which uses formula scripts to define installation procedures.

$ greet -n "World" -l fr
Bonjour, World

You can reuse existing application resources as standard CLI tools without writing additional code. Through Homebrew distribution, users can utilize these tools like any other command-line tool, without needing to know they’re powered by PHP or BEAR.Sunday.

Installation

Install using Composer:

composer require bear/cli

Basic Usage

Adding CLI Attributes to Resources

Add CLI attributes to your resource class to define the command-line interface:

use BEAR\Cli\Attribute\Cli;
use BEAR\Cli\Attribute\Option;

class Greeting extends ResourceObject
{
    #[Cli(
        name: 'greet',
        description: 'Say hello in multiple languages',
        output: 'greeting'
    )]
    public function onGet(
        #[Option(shortName: 'n', description: 'Name to greet')]
        string $name,
        #[Option(shortName: 'l', description: 'Language (en, ja, fr, es)')]
        string $lang = 'en'
    ): static {
        $greeting = match ($lang) {
            'ja' => 'こんにちは',
            'fr' => 'Bonjour',
            'es' => '¡Hola',
            default => 'Hello',
        };
        $this->body = [
            'greeting' => "{$greeting}, {$name}",
            'lang' => $lang
        ];

        return $this;
    }
}

Generating CLI Commands and Formula

To convert a resource into a command, run the following command with your application name (vendor name and project name):

$ vendor/bin/bear-cli-gen 'MyVendor\MyProject'
# Generated files:
#   bin/cli/greet         # CLI command
#   var/homebrew/greet.rb # Homebrew formula

Note: Homebrew formula is generated only when a GitHub repository is configured.

Command Usage

The generated command provides standard CLI features such as:

Displaying Help

$ greet --help
Say hello in multiple languages

Usage: greet [options]

Options:
  --name, -n     Name to greet (required)
  --lang, -l     Language (en, ja, fr, es) (default: en)
  --help, -h     Show this help message
  --version, -v  Show version information
  --format       Output format (text|json) (default: text)

Showing Version Information

$ greet --version
greet version 0.1.0

Basic Usage Examples

# Basic greeting
$ greet -n "World"
Hello, World

# Specify language
$ greet -n "World" -l ja
こんにちは, World

# Short options
$ greet -n "World" -l fr
Bonjour, World

# Long options
$ greet --name "World" --lang es
¡Hola, World

JSON Output

$ greet -n "World" -l ja --format json
{
    "greeting": "こんにちは, World",
    "lang": "ja"
}

Output Behavior

CLI command output follows these specifications:

  • Default output: Displays only the specified field value
  • --format=json option: Displays full JSON response similar to API endpoint
  • Error messages: Output to standard error (stderr)
  • HTTP status code mapping: Maps to exit codes (0: success, 1: client error, 2: server error)

Distribution

Commands created with BEAR.Cli can be distributed via Homebrew. Formula generation requires the application to be published on GitHub:

1. Local Formula Distribution

For testing development versions:

$ brew install --formula ./var/homebrew/greet.rb

2. Homebrew Tap Distribution

Method for wide distribution using a public repository:

Note: The file name of the formula and the class name inside it are based on the name of the repository. For example, if the GH repository is koriym/greet, then var/homebrew/greet.rb will be generated, which contains the Greet class. In this case, greet will be the name of the tap that is published, but if you want to change it, please change the class name and file name of fomula script.

$ brew tap your-vendor/greet
$ brew install your-vendor/greet

This method is particularly suitable for:

  • Open source projects
  • Continuous updates provision

Testing Development Version

$ brew install --HEAD ./var/homebrew/greet.rb
$ greet --version
greet version 0.1.0

Stable Release

  1. Create a tag:
    $ git tag -a v0.1.0 -m "Initial stable release"
    $ git push origin v0.1.0
    
  2. Update formula:
     class Greet < Formula
    +  desc "Your CLI tool description"
    +  homepage "https://github.com/your-vendor/greet"
    +  url "https://github.com/your-vendor/greet/archive/refs/tags/v0.1.0.tar.gz"
    +  sha256 "..." # Add hash value obtained from the command below
    +  version "0.1.0"
    head "https://github.com/your-vendor/greet.git", branch: "main"
       
    depends_on "php@8.1"
    depends_on "composer"
     end
    

You can add dependencies like databases to the formula as needed. However, it’s recommended to handle database setup and other environment configuration in the bin/setup script.

  1. Get SHA256 hash:
    # Download tarball from GitHub and calculate hash
    $ curl -sL https://github.com/your-vendor/greet/archive/refs/tags/v0.1.0.tar.gz | shasum -a 256
    
  2. Create Homebrew tap: Create a repository using GitHub CLI(gh) or github.com/new. The public repository name must start with homebrew-, for example homebrew-greet:
    $ gh auth login
    $ gh repo create your-vendor/homebrew-greet --public --clone
    # Or create and clone repository using the web interface
    $ cd homebrew-greet
    
  3. Place and publish formula:
    $ cp /path/to/project/var/homebrew/greet.rb .
    $ git add greet.rb
    $ git commit -m "Add formula for greet command"
    $ git push
    
  4. Installation and distribution: End users can start using the tool with just these commands. PHP environment and dependency package installation are handled automatically, so users don’t need to worry about environment setup:
    $ brew tap your-vendor/greet    # homebrew- prefix can be omitted
    $ brew install your-vendor/greet
    # Ready to use immediately
    $ greet --version
    greet version 0.1.0
    

Formula Customization

You can edit the formula using the brew edit command as needed:

$ brew edit your-vendor/greet
class Greet < Formula
  desc "Your CLI tool description"
  homepage "https://github.com/your-vendor/greet"
  url "https://github.com/your-vendor/greet/archive/refs/tags/v0.1.0.tar.gz"
  sha256 "..." # tgz SHA256
  version "0.1.0"
  
  depends_on "php@8.4"  # Specify PHP version
  depends_on "composer"

  # Add if required by the application
  # depends_on "mysql"
  # depends_on "redis"
end

Clean Architecture

BEAR.Cli demonstrates the strengths of both Resource Oriented Architecture (ROA) and Clean Architecture. Following Clean Architecture’s principle that “UI is a detail,” you can add CLI as a new adapter alongside the web interface for the same resource.

Furthermore, BEAR.Cli supports not only command creation but also distribution and updates through Homebrew. This allows end users to start using tools with a single command, treating them as native UNIX commands without awareness of PHP or BEAR.Sunday.

Additionally, CLI tools can be version-controlled and updated independently from the application repository. This means they can maintain stability and continuous updates as command-line tools without being affected by API evolution. This represents a new form of API delivery, realized through the combination of Resource Oriented Architecture and Clean Architecture.


HTML

The following template engines are available for HTML representation.

Twig vs Qiq

Twig was first released in 2009 and has a large user base. Qiq is a new template engine released in 2021.

Twig uses implicit escaping by default and has custom syntax for control structures. In contrast, Qiq requires explicit escaping and uses PHP syntax as the base template language. Twig has a large codebase and rich features, while Qiq is compact and simple. (Using pure PHP syntax in Qiq makes it IDE and static analysis-friendly, although it may be redundant.)

Syntax Comparison

PHP

<?= htmlspecialchars($var, ENT_QUOTES|ENT_DISALLOWED, 'utf-8') ?>
<?= htmlspecialchars(helper($var, ENT_QUOTES|ENT_DISALLOWED, 'utf-8')) ?>
<?php foreach ($users => $user): ?>
 * <?= $user->name; ?>
<?php endforeach; ?>

Twig

{{ var | raw }}
{{ var }}
{{ var | helper }}
{% for user in users %}
  * {{ user.name }}
{% endfor %}

Qiq

{{% var }}
{{h $var }}
{{h helper($var) }}
{{ foreach($users => $user) }}
  * {{h $user->name }}
{{ endforeach }}

{{ var }} // Not displayed 
<?php /** @var Template $this */ ?>
<?= $this->h($var) ?>

Renderer

The renderer, bound to RenderInterface and injected into the ResourceObject, generates the representation of the resource. The resource itself is agnostic about its representation.

Since the renderer is injected per resource, it is possible to use multiple template engines simultaneously.

Halo UI for Development

During development, you can render a UI element called Halo 24 around the rendered resource. Halo provides information about the resource’s state, representation, and applied interceptors. It also provides links to open the corresponding resource class or resource template in PHPStorm.

Halo displays resource state

  • Halo Home (Border and Tools Display)
  • Resource State
  • Resource Representation
  • Profile

You can try a demo of Halo in the demo.

Performance Monitoring

Halo also displays performance information about the resource, including execution time, memory usage, and a link to the profiler.

Halo displays performance

Installation

To enable profiling, you need to install xhprof, 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. Example: Call Graph Demo

// 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.

class DevModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new HaloModule($this));
    }
}


Form

Ray.WebFormModule provides aspect-oriented web form validation powered by Aura.Input and Ray.Di. Form fields, validation rules, submitted values, and rendering helpers are collected in a single form class so the form is easy to test and change.

Installation

Install ray/web-form-module with Composer.

composer require ray/web-form-module

Install WebFormModule in your application module.

use Ray\Di\AbstractModule;
use Ray\WebFormModule\WebFormModule;

class AppModule extends AbstractModule
{
    protected function configure()
    {
        $this->install(new WebFormModule());
    }
}

The legacy Ray\WebFormModule\AuraInputModule class remains available as a thin subclass of WebFormModule for backward compatibility. New code should use WebFormModule.

Form Class

A self-initializing form class defines fields and validation rules in init(). If the form implements submit(), the returned values are used as submitted data. See Aura.Input self-initializing forms for the underlying form API.

use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\SetAntiCsrfTrait;

class MyForm extends AbstractForm
{
    use SetAntiCsrfTrait;

    public function init()
    {
        $this->setField('name', 'text')
             ->setAttribs([
                 'id' => 'name'
             ]);

        $this->filter->validate('name')->is('alnum');
        $this->filter->useFieldMessage('name', 'Name must be alphanumeric only.');
    }

    public function submit()
    {
        return $_POST;
    }

    public function __toString()
    {
        $form = $this->form();
        $form .= $this->helper->tag('div', ['class' => 'form-group']);
        $form .= $this->helper->tag('label', ['for' => 'name']);
        $form .= 'Name:';
        $form .= $this->helper->tag('/label') . PHP_EOL;
        $form .= $this->input('name');
        $form .= $this->error('name');
        $form .= $this->helper->tag('/div') . PHP_EOL;
        $form .= $this->input('submit');
        $form .= $this->helper->tag('/form');

        return $form;
    }
}

Controller

Annotate methods that require form validation with #[FormValidation]. The form argument names the form property on the controller, and onFailure names the method called when validation fails.

use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use Ray\WebFormModule\Annotation\FormValidation;
use Ray\WebFormModule\FormInterface;

class MyController
{
    /** @var FormInterface */
    protected $contactForm;

    #[Inject]
    public function setForm(#[Named("contact_form")] FormInterface $form)
    {
        $this->contactForm = $form;
    }

    #[FormValidation(form: "contactForm", onFailure: "badRequestAction")]
    public function createAction()
    {
        // validation success
        // More detail for vnd.error+json can be added with #[VndError].
    }

    public function badRequestAction()
    {
        // validation failed
    }
}

View

When the form provides string rendering, echoing the form renders the complete HTML.

echo $form;

You can also render individual inputs and errors.

echo $form->input('name'); // <input id="name" type="text" name="name" size="20" maxlength="20" />
echo $form->error('name'); // "Name must be alphanumeric only." or blank.

CSRF Protections

CSRF protection is opt-in and can be enabled through either of two independent paths:

  • Per-form: add use SetAntiCsrfTrait; to the form. AntiCsrfInterface is injected at construction time, the token field is added in postConstruct(), and every apply() call verifies the token.
  • Per-action: annotate the validated method with #[CsrfProtection]. AuraInputInterceptor then injects AntiCsrfInterface into the form before apply() runs.

Either path causes AbstractForm::apply() to throw CsrfViolationException on token mismatch. Without either path, no CSRF check is performed.

use Ray\WebFormModule\AbstractForm;
use Ray\WebFormModule\Annotation\CsrfProtection;
use Ray\WebFormModule\Annotation\FormValidation;
use Ray\WebFormModule\SetAntiCsrfTrait;

class MyForm extends AbstractForm
{
    use SetAntiCsrfTrait;
}

class MyController
{
    #[FormValidation(form: "contactForm")]
    #[CsrfProtection]
    public function createAction()
    {
    }
}

You can provide a custom AntiCsrf class. See Applying CSRF Protections in Aura.Input for details.

Migration From 0.x

Version 1.0 drops Doctrine Annotations in favor of native PHP 8 attributes and tightens type declarations. The most common rewrites are:

Before (0.x) After (1.0)
@FormValidation(form="f", onFailure="badRequest") #[FormValidation(form: 'f', onFailure: 'badRequest')]
@FormValidation(form="f", antiCsrf=true) #[FormValidation(form: 'f')] + #[CsrfProtection]
@InputValidation(form="f") #[InputValidation(form: 'f')]
@VndError(message="...", logref="...") #[VndError(message: '...', logref: '...')]
new AuraInputInterceptor($injector, $reader) new AuraInputInterceptor($injector)
public function input($input) / public function error($input) input(string $input): string / error(string $input): string

See CHANGELOG.md for the full list of breaking changes.

Automated Migration With Claude Code

Ray.WebFormModule ships a Claude Code skill at .claude/skills/migrate-to-1.0/SKILL.md that walks an AI assistant through the rewrites above: annotations to attributes, antiCsrf=true split into #[CsrfProtection], Reader argument removal, and FormInterface signature updates. Copy that directory into your consuming project’s .claude/skills/ and invoke it with /migrate-to-1.0.

Validation Exception

#[InputValidation] throws Ray\WebFormModule\Exception\ValidationException when validation fails. This is useful for API applications where the HTML representation is not used.

use Ray\WebFormModule\Annotation\InputValidation;

class Foo
{
    #[InputValidation(form: "form1")]
    public function createAction($name)
    {
        // ...
    }
}

Installing Ray\WebFormModule\FormVndErrorModule makes methods annotated with #[FormValidation] throw the same validation exception on failure.

use Ray\Di\AbstractModule;
use Ray\WebFormModule\FormVndErrorModule;
use Ray\WebFormModule\WebFormModule;

class FakeVndErrorModule extends AbstractModule
{
    protected function configure()
    {
        $this->install(new WebFormModule());
        $this->override(new FormVndErrorModule());
    }
}

Echo the caught exception’s error property to get an application/vnd.error+json representation.

echo $e->error;

//{
//    "message": "Validation failed",
//    "path": "/path/to/error",
//    "validation_messages": {
//        "name": [
//            "Name must be alphanumeric only."
//        ]
//    }
//}

Add more detail to vnd.error+json with the #[VndError] attribute.

#[FormValidation(form: "contactForm")]
#[VndError(message: "foo validation failed", logref: "a1000", path: "/path/to/error", href: ["_self" => "/path/to/error", "help" => "/path/to/help"])]
public function createAction()
{
}

Demo

Run the demo application from the Ray.WebFormModule repository.

php -S docs/demo/1.csrf/web.php

Content Negotiation

In HTTP, 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 with composer

composer require bear/accept

Next, save the context corresponding to the Accept * request header in /var/locale/available.php

<?php
return [
    'Accept' => [
        '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
use BEAR\Accept\Accept;

require dirname(__DIR__) . '/vendor/autoload.php';

$accept = new Accept(require dirname(__DIR__) . '/var/locale/available.php');
list($context, $vary) = $accept($_SERVER);

require dirname(__DIR__) . '/bootstrap/bootstrap.php';

For example, in the above setting, the access context of the following Accept* header will be prod-hal-ja-app.

Accept: application/hal+json
Accept-Language: ja-JP

At this time JaModule requires binding for Japanese text. For details, refer to the demo application MyVendor.Locale.

Enable by Resource

To do content negotiation on a resource basis, install the AcceptModule module and use the #[Produces] attribute.

Module Install

protected function configure()
{
    // ...
    $available = $appDir . '/var/locale/available.php';
    $this->install(new AcceptModule($available));
}

#[Produces] Attribute

use BEAR\Accept\Attribute\Produces;

#[Produces(['application/hal+json', 'text/csv'])]
public function onGet()

Specify available media types 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 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.

Resources should have a self URI

{
    "_links": {
        "self": { "href": "/user" }
    }
}

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 <link> and <a> tag.

{
    "_links": {
        "next": { "href": "/page=2" }
    }
}

For more information about HAL please visit http://stateless.co/hal_specification.html.

Resource Class

You can annotate links and embed other resources.

You can declaratively describe the @Link annotation, or dynamic ones are assigned to body['_links'].

#[Link(rel="user", href="/user")]
#[Link(rel="latest-post", href="/latest-post", title="latest post entrty")]
public function onGet()

or

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.

#[Embed(rel="todos", src="/todos{?status}")]
#[Embed(rel="me", src="/me")]
public function onGet(string $status): static

or

$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
namespace MyVendor\MyPorject\Resource\Page\Rels;

use BEAR\ApiDoc\ApiDoc;

class Index extends ApiDoc
{
}

Publish the folder of JSON Schema to the web

ln -s var/json_schema public/schemas

API documents are automatically generated using Docblock comments and JSON Schema. The page class has its own renderer and is not affected by $context, it serves a document (text/html) for people. Since it is not affected by $context, you can install either App or Page.

If CURIEs is installed at the root, the API itself can be used even for raw JSON which is not hypermedia. Documents generated in real time always accurately reflect property information and validation constraints.

Run demo

git clone https://github.com/koriym/Polidog.Todo.git
cd Polidog.Todo/
composer install
composer setup
composer doc

Open docs/index.md to see API doc page.

Browsable

The API set written in HAL functions as headless REST application.

You can access all the resources by following the link from the root like the website with the Web-based HAL Browser or the CURL command of the console.

HAL Layout Beta

Libraries for rendering HAL resources as React/Vue components:

Siren

Siren Module is also available for Siren hypermedia (application/vnd.siren+json) type.


PSR-7

You can get server side request information using PSR7 HTTP message interface. Also, you can run BEAR.Sunday application as PSR 7 middleware.

HTTP Request

PHP has Superglobals such as $_SERVER and $_COOKIE, but instead of using them it receives server side request information using the PSR-7 HTTP message interface.

ServerRequest (general)

class Index extends ResourceObject
{
    public function __construct(ServerRequestInterface $serverRequest)
    {
        // retrieve cookies
        $cookie = $serverRequest->getCookieParams(); // $_COOKIE
    }
}

Upload Files


use Psr\Http\Message\UploadedFileInterface;
use Ray\HttpMessage\Annotation\UploadFiles;

class Index extends ResourceObject
{
    /**
     * @UploadFiles
     */
    public function __construct(array $files)
    {
        // retrieve file name
        $file = $files['my-form']['details']['avatar'][0]
        /* @var UploadedFileInterface $file */
        $name = $file->getClientFilename(); // my-avatar3.png
    }
}

URI


use Psr\Http\Message\UriInterface;

class Index extends ResourceObject
{
    public function __construct(UriInterface $uri)
    {
        // retrieve host name
        $host = $uri->getHost();
    }
}

PSR-7

An existing BEAR.Sunday application can work as a PSR-7 middleware with these easy steps:

1) Add bear/middleware package then replace bootstrap.php script.

composer require bear/middleware
cp vendor/bear/middleware/bootstrap/bootstrap.php bootstrap/bootstrap.php

2) Replace __PACKAGE__\__VENDOR__ in bootstrap.php to application namespace.

Stat the server.

php -S 127.0.0.1:8080 -t public

Stream

BEAR.Sunday supports HTTP body of a message output in a stream.

In ResourceObject, you can mix stream with a normal string. The output is converted to a single stream. StreamTransfer is the default HTTP transfer. Seem more at Stream Response.

New Project

You can also create a BEAR.Sunday PSR-7 project with bear/project from scratch.

composer create-project bear/project my-psr7-project
cd my-psr7-project/
php -S 127.0.0.1:8080 -t public

PSR-7 middleware


JavaScript SSR

Instead of rendering views with PHP template engines such as Twig, this module enables server-side JavaScript rendering. PHP handles authorization, authentication, initial state, and API delivery, while JavaScript renders the UI. Only resources with the #[Ssr] attribute are affected, making adoption straightforward within existing projects.

Background and Use Cases

This module was developed to enable JavaScript server-side rendering (SSR) within PHP applications.

Today, JavaScript ecosystem frameworks like Next.js, Nuxt, and Remix provide mature solutions for SSR entirely within JavaScript. For new projects with JavaScript-centric UIs, these frameworks are typically the preferred choice.

This module is suited for the following scenarios:

  • Rendering most pages with your existing template engine, while using JS UI only for pages requiring high interactivity
  • Adding React or Vue.js UI to specific pages in an existing BEAR.Sunday project
  • Maintaining a single PHP application without separating frontend and backend
  • Enabling PHP and JS teams to develop in parallel using state/metas as the contract

Only resources with the #[Ssr] attribute are rendered with JS UI, allowing easy coexistence with traditional template engines.

Prerequisites

Note: If you do not install V8Js then JS will be run using Node.js.

JavaScript

Installation

Install bear/ssr-module into the project.

// composer create-project bear/skeleton MyVendor.MyProject && cd MyVendor.MyProject;
composer require bear/ssr-module

Install the UI skeleton app koriym/js-ui-skeleton.

composer require koriym/js-ui-skeleton 1.x-dev
cp -r vendor/koriym/js-ui-skeleton/ui .
cp -r vendor/koriym/js-ui-skeleton/package.json .
npm install

Running the UI application

Lets start by running the demo application. From the displayed web page lets select the rendering engine and run the JS application.

npm run ui

This applications inputs can be set using the ui/dev/config/ config files.

<?php
$app = 'index';                   // =index.bundle.js
$state = [                        // Application state
    'hello' =>['name' => 'World']
];
$metas = [                        // value used in SSR only
    'title' =>'page-title'
];

return [$app, $state, $metas];

Lets copy the configuration file and try changing the input values.

cp ui/dev/config/index.php ui/dev/config/myapp.php

Reload the browser and try out the new settings.

In this way without changing the JavaScript or core PHP application we can alter the UI data and check that it is working.

The PHP configuration files that have been edited in this section are only used when executing npm run ui. All the PHP side needs is the output bundled JS file.

Creating the UI application.

Using the variables that have been passed in from PHP, create a render function that returns a rendered string.

const render = (state, metas) => (
  __AWESOME_UI__ // Using a SSR compatible library or JS template engine return an output string.
)

The state value is needed in the document root, metas contains other variables, such as those needed in <head>. The render function name cannot be changed.

Here we can grab the name and create a greeting string to be returned.

const render = state => (
  `Hello ${state.name}`
)

Save this as ui/src/page/hello/server.js and register this as a Webpack entry point in ui/entry.js.

module.exports = {
  hello: 'src/page/hello/server',
};

Having done this a hello.bundle.js bundled file is created for us.

Create a file at ui/dev/config/myapp.php to test run this application.

<?php
$app = 'hello';
$state = ['name' => 'World'];
$metas = [];

return [$app, $state, $metas];

Thats it! Reload the browser to try it out.

Inside the render function you can use any UI framework such as React or Vue.js to create a rich UI. In a regular application in order to limit the number of dependencies in the server.js entry file import the render module as below.

import render from './render';
global.render = render;

Thus far there has been nothing happening on the PHP side. Development on the SSR application and PHP development can done independently.

PHP

Module Installation

Install SsrModule in AppModule.

<?php
use BEAR\SsrModule\SsrModule;

class AppModule extends AbstractAppModule
{
    protected function configure()
    {
        // ...
        $build = dirname(__DIR__, 2) . '/var/www/build';
        $this->install(new SsrModule($build));
    }
}

The $build directory is where the JS files live.(The Webpack output location set in ui/ui.config.js)

#[Ssr] Attribute

Annotate the resource function to be SSR’d with #[Ssr]. The JS application name is required in app.

<?php

namespace MyVendor\MyRedux\Resource\Page;

use BEAR\Resource\ResourceObject;
use BEAR\SsrModule\Annotation\Ssr;

class Index extends ResourceObject
{
    #[Ssr(app: 'index_ssr')]
    public function onGet(string $name = 'BEAR.Sunday'): static
    {
        $this->body = [
            'hello' => ['name' => $name]
        ];

        return $this;
    }
}

When you want to pass in distinct values for SSR and CSR set a key in state and metas.

#[Ssr(app: 'index_ssr', state: ['name', 'age'], metas: ['title'])]
public function onGet(): static
{
    $this->body = [
        'name' => 'World',
        'age' => 4.6E8,
        'title' => 'Age of the World'
    ];

    return $this;
}

To see exactly how you pass in state and metas to achieve SSR see the sample application ui/src/page/index/server. The only influence is from the annotated method, the rest comes straight from the API or HTML rendering configuration.

Runtime PHP Application Settings

Edit ui/ui.config.js, set the Webpack build location in build and web directory in public. The build directory is the same that you set in the SsrModule installation.

const path = require('path');

module.exports = {
  public: path.join(__dirname, '../var/www'),
  build: path.join(__dirname, '../var/www/build')
};

Running the PHP application

npm run dev

Run using live updating. When the PHP file is changed it will be automatically reloaded, if there is a change in a React component without hitting refresh the component will update. If you want to run the app without live updating you can by running npm run start.

For other commands such lint or test etc. please see commands.

Performance

The V8 snapshot can be saved to APCu for improved performance. Install ApcSsrModule in ProdModule. V8Js is required:

$bundleSrcBasePath = dirname(__DIR__, 2) . '/var/www/build';
$this->install(new ApcSsrModule($bundleSrcBasePath));

The $bundleSrcBasePath is the directory path where the JavaScript bundle files are located.


Stream Response

Normally, resources are rendered by renderers into one string and finally echoed 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 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.

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

<!DOCTYPE html>
<html lang="en">
<body>
<p>Hello, {{ name }}</p>
<img src="data:image/jpg;base64,{{ image }}">
</body>
</html>

name assigns the string as usual, but assigns the resource variable of the image file’s pointer resource to image with the base64-encode filter.

class Image extends ResourceObject
{
    use StreamTransferInject;

    public function onGet(string $name = 'inline image'): static
    {
        $fp = fopen(__DIR__ . '/image.jpg', 'r');
        stream_filter_append($fp, 'convert.base64-encode'); // image base64 format
        $this->body = [
            'name' => $name,
            'image' => $fp
        ];

        return $this;
    }
}

If you want to further control streaming such as streaming bandwidth and timing control, uploading to the cloud, etc use StreamResponder which is build for it.

The demo is available at MyVendor.Stream.


This document needs to be proofread by native speaker.


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.

distributed cache

Tag-based Cache Invalidation

dependency graph 2021-10-19 21 38 02

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

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.

image

In this case, since the entire content is dynamic, the whole donut is not cached. Therefore, no ETag is output either.

Donut Hole Cache

image

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

recursive donut 2021-10-19 21 27 06

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.

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

conditional request

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:

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

recursive donut 2021-10-19 21 27 06

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.

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

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.

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.

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.

interface DonutRepositoryInterface
{
    public function purge(AbstractUri $uri): void;
    public function invalidateTags(array $tags): void;
}

Invalidate by URI

// example
$this->repository->purge(new Uri('app://self/blog/comment'));

Disable by tag

$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.

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.

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.

public function __construct(private UriTagInterface $uriTag)
{}
$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.

// 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.

$this->headers[Header::SURROGATE_KEY] = $this->uriTag->fromAssoc(
    uriTemplate: 'app://self/item{?id}',
    assoc: $this->body
);

In the above case, this cache will be invalidated for both server-side and CDN when app://self/item?id=1 and app://self/item?id=2 are changed.

Configuration

Redis Marshaller

The Redis cache adapter allows you to configure data compression and serialization methods.

A marshaller handles the serialization of PHP objects and arrays when storing them in Redis, and deserialization when retrieving them.

use BEAR\QueryRepository\StorageRedisDsnModule;

$this->install(
    new StorageRedisDsnModule(
        dsn: 'redis://localhost:6379',
        marshallingOptions: [
            'enabled' => true,
            'type' => 'deflate',      // 'default' or 'deflate'
            'use_igbinary' => true    // requires ext-igbinary
        ]
    )
);

Marshaller types:

  • default: Uses PHP’s standard serialization (enabling use_igbinary uses a more efficient binary format)
  • deflate: Compresses data before storing (uses zlib)

Use deflate if you want to reduce Redis memory usage. This is a trade-off with CPU usage.

CDN

If you install a module that supports a specific CDN, vendor-specific headers will be output.

$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.

multi cdn diagram

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.

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.

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 <a> tag or when a URL is entered.

ResponseHeader::CACHE_CONTROL => 'max-age=60'

If response time is important to you, consider specifying SWR.

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.

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.

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/)

***

# High-Performance Servers

BEAR.Sunday applications can run on high-performance PHP servers that eliminate per-request bootstrap overhead. This guide covers three server options: Swoole, RoadRunner, and FrankenPHP.

## Overview

In traditional PHP-FPM, each request bootstraps the entire application:

```text
Request -> Boot Framework -> Route -> Execute -> Response -> Shutdown
Request -> Boot Framework -> Route -> Execute -> Response -> Shutdown
Request -> Boot Framework -> Route -> Execute -> Response -> Shutdown

With persistent worker mode, the application boots once:

Boot Framework (once)
    |
Request -> Route -> Execute -> Response
Request -> Route -> Execute -> Response
Request -> Route -> Execute -> Response

This eliminates boot overhead, resulting in significantly lower latency and higher throughput.

BEAR.Sunday’s stateless resource design and immutable architecture are well-suited for persistent worker environments, enabling seamless transition to worker mode without global state issues.

Server Comparison

Feature Swoole RoadRunner FrankenPHP
Language C + PHP Go + PHP Go + PHP
Worker Mode Yes Yes Yes
HTTP/2 Yes Yes Yes
HTTP/3 No No Yes
WebSocket Native Native Via Caddy
Coroutines Yes No No
Hot Reload Manual Yes Yes
Memory Limit Shared Per worker Per worker

Quick Start with Docker

The bear-sunday-servers repository provides ready-to-use Docker configurations for all three servers.

git clone https://github.com/bearsunday/bear-sunday-servers.git
cd bear-sunday-servers

# Swoole (port 8081)
cd swoole && docker compose up -d && curl http://localhost:8081/

# RoadRunner (port 8082)
cd roadrunner && docker compose up -d && curl http://localhost:8082/

# FrankenPHP (port 8080)
cd frankenphp && docker compose up -d && curl http://localhost:8080/

Swoole

Swoole is a coroutine-based PHP extension providing event-driven asynchronous I/O.

Features

  • Event-Driven: Asynchronous I/O handling
  • Coroutines: Concurrent request processing without threads
  • Request Isolation: Coroutine-context request isolation
  • High Performance: Eliminates per-request boot overhead
  • Memory Efficient: Shared memory between workers

Install

Swoole Extension (ext-swoole ^6.1)

pecl install swoole

Or compile from source:

git clone https://github.com/swoole/swoole-src.git && \
cd swoole-src && \
phpize && \
./configure && \
make && make install

Add extension=swoole.so to your php.ini.

BEAR.Swoole Package

composer require bear/swoole

Bootstrap Script

Create bin/swoole.php:

<?php

declare(strict_types=1);

require dirname(__DIR__) . '/autoload.php';

$bootstrap = dirname(__DIR__) . '/vendor/bear/swoole/bootstrap.php';

$context = getenv('BEAR_CONTEXT') ?: 'prod-hal-app';
$ip = getenv('SWOOLE_IP') ?: '0.0.0.0';
$port = (int) (getenv('SWOOLE_PORT') ?: 8080);

exit((require $bootstrap)(
    $context,
    'MyVendor\MyProject',
    $ip,
    $port
));

Run

php bin/swoole.php
Swoole http server is started at http://127.0.0.1:8080

Environment Variables

Variable Default Description
BEAR_CONTEXT prod-hal-app BEAR.Sunday context
SWOOLE_IP 0.0.0.0 Server bind address
SWOOLE_PORT 8080 Server port

Architecture

Master Process
    |
    +-- Manager Process
           |
           +-- Worker 1 (coroutines)
           +-- Worker 2 (coroutines)
           +-- Worker N (coroutines)

Each worker can handle multiple concurrent requests using coroutines.

Development Notes

Xdebug is not fully compatible with Swoole’s coroutines. For debugging:

  • Use var_dump() / error_log() for simple debugging
  • Or disable Swoole and use PHP’s built-in server with Xdebug

Swoole does not support automatic hot reload. Restart after code changes:

# With Docker
docker compose restart

# Without Docker
pkill -f swoole.php && php bin/swoole.php

Parallel Execution

Swoole server involves two independent concerns:

  • Server: How to run the application (this page)
  • Parallel Execution: How to execute embedded resources concurrently

With BEAR.Async, #[Embed] resources are automatically executed in parallel using Swoole coroutines. See Parallel Resource Execution for details.


RoadRunner

RoadRunner is a high-performance Go application server with PSR-7 PHP workers.

Features

  • Go Application Server: High-performance process manager
  • PSR-7 Workers: Standard HTTP message interface
  • Built-in Metrics: Prometheus-compatible endpoint
  • Hot Reload: Automatic worker restart on file changes

Install

RoadRunner Binary

Download from releases or use Docker.

PHP Dependencies

composer require spiral/roadrunner-http nyholm/psr7

Configuration

Create .rr.yaml:

version: "3"

server:
  command: "php bin/worker.php"
  relay: pipes

http:
  address: "0.0.0.0:8082"
  pool:
    num_workers: 4
    max_jobs: 1000
    allocate_timeout: 60s
    destroy_timeout: 60s

logs:
  mode: production
  level: info
  output: stdout

status:
  address: "0.0.0.0:2112"

Worker Script

Create bin/worker.php:

<?php

declare(strict_types=1);

use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Extension\Application\AppInterface;
use MyVendor\MyProject\Injector;
use MyVendor\MyProject\Module\App;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\Worker;

require dirname(__DIR__) . '/autoload.php';

// Get configuration from environment
$context = getenv('BEAR_CONTEXT') ?: 'prod-hal-app';
$maxRequests = (int) (getenv('MAX_REQUESTS') ?: 1000);

// Boot application once (outside the request loop)
$app = Injector::getInstance($context)->getInstance(AppInterface::class);
assert($app instanceof App);

// Create RoadRunner worker
$worker = Worker::create();
$factory = new Psr17Factory();
$psr7Worker = new PSR7Worker($worker, $factory, $factory, $factory);

$requestCount = 0;

while ($psrRequest = $psr7Worker->waitRequest()) {
    try {
        if (! $psrRequest instanceof ServerRequestInterface) {
            break;
        }

        // Convert PSR-7 request to $_SERVER format
        $server = createServerVars($psrRequest);
        $globals = createGlobals($psrRequest);

        // Route and execute request
        $request = $app->router->match($globals, $server);
        $response = $app->resource->{$request->method}->uri($request->path)($request->query);
        assert($response instanceof ResourceObject);

        // Convert ResourceObject to PSR-7 Response
        $psrResponse = $factory->createResponse($response->code);
        foreach ($response->headers as $name => $value) {
            $psrResponse = $psrResponse->withHeader($name, $value);
        }
        $psrResponse = $psrResponse->withBody($factory->createStream((string) $response));

        $psr7Worker->respond($psrResponse);
    } catch (Throwable $e) {
        $psr7Worker->respond($factory->createResponse(500)->withBody(
            $factory->createStream($e->getMessage())
        ));
    }

    gc_collect_cycles();
    $requestCount++;

    if ($maxRequests > 0 && $requestCount >= $maxRequests) {
        break;
    }
}

function createServerVars(ServerRequestInterface $request): array
{
    $uri = $request->getUri();
    $server = $request->getServerParams();

    $server['REQUEST_METHOD'] = $request->getMethod();
    $server['REQUEST_URI'] = $uri->getPath() . ($uri->getQuery() ? '?' . $uri->getQuery() : '');
    $server['QUERY_STRING'] = $uri->getQuery();
    $server['HTTP_HOST'] = $uri->getHost();

    foreach ($request->getHeaders() as $name => $values) {
        $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
        $server[$key] = implode(', ', $values);
    }

    return $server;
}

function createGlobals(ServerRequestInterface $request): array
{
    return [
        '_GET' => $request->getQueryParams(),
        '_POST' => (array) $request->getParsedBody(),
        '_COOKIE' => $request->getCookieParams(),
        '_FILES' => $request->getUploadedFiles(),
        '_SERVER' => createServerVars($request),
    ];
}

Run

./rr serve -c .rr.yaml

Environment Variables

Variable Default Description
BEAR_CONTEXT prod-hal-app BEAR.Sunday context
MAX_REQUESTS 1000 Requests before worker restart

Architecture

RoadRunner (Go)
    |
    +-- PHP Worker 1 (persistent)
    +-- PHP Worker 2 (persistent)
    +-- PHP Worker N (persistent)

Each worker boots BEAR.Sunday once and handles requests via pipes.

Metrics

Prometheus metrics available at http://localhost:2112/metrics.


FrankenPHP

FrankenPHP is a modern PHP application server built on Caddy with worker mode support.

Features

  • Worker Mode: Eliminates application boot cost per request
  • HTTP/2 & HTTP/3: Automatic HTTPS with Caddy
  • Production Ready: OPcache JIT, multi-stage builds
  • Development Ready: Xdebug, hot reload

Install

FrankenPHP is typically used via Docker. For standalone installation, see FrankenPHP documentation.

Worker Script

Create bin/worker.php:

<?php

declare(strict_types=1);

use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Extension\Application\AppInterface;
use MyVendor\MyProject\Injector;
use MyVendor\MyProject\Module\App;

require dirname(__DIR__) . '/autoload.php';

// Get configuration from environment
$context = getenv('BEAR_CONTEXT') ?: 'prod-hal-app';
$maxRequests = (int) (getenv('MAX_REQUESTS') ?: 1000);

// Boot application once (outside the request loop)
$app = Injector::getInstance($context)->getInstance(AppInterface::class);
assert($app instanceof App);

$requestCount = 0;

// FrankenPHP worker loop
// Superglobals ($_GET, $_POST, $_SERVER) are automatically reset
do {
    $running = frankenphp_handle_request(static function () use ($app): void {
        try {
            // Check HTTP cache
            if ($app->httpCache->isNotModified($_SERVER)) {
                $app->httpCache->transfer();
                return;
            }

            // Route and execute request
            $request = $app->router->match($GLOBALS, $_SERVER);
            $response = $app->resource->{$request->method}->uri($request->path)($request->query);
            assert($response instanceof ResourceObject);
            $response->transfer($app->responder, $_SERVER);
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request ?? null)->transfer();
        }

        gc_collect_cycles();
    });

    $requestCount++;

    if ($maxRequests > 0 && $requestCount >= $maxRequests) {
        break;
    }
} while ($running);

Caddyfile Configuration

{
    admin off

    frankenphp {
        worker /app/bin/worker.php {
            num {$FRANKENPHP_NUM_WORKERS:4}
        }
    }
}

{$SERVER_NAME::8080} {
    root * /app/public

    encode zstd br gzip

    respond /health 200

    php_server

    log {
        output stdout
        format console
    }
}

Run with Docker

docker run -v $PWD:/app -p 8080:8080 dunglas/frankenphp

Environment Variables

Variable Default Description
BEAR_CONTEXT prod-hal-app BEAR.Sunday context
MAX_REQUESTS 1000 Requests before worker restart
SERVER_NAME :8080 Listen address
FRANKENPHP_NUM_WORKERS 4 Number of worker processes

Memory Management

  • Workers automatically restart after MAX_REQUESTS to prevent memory leaks
  • gc_collect_cycles() runs after each request
  • Set MAX_REQUESTS=0 for unlimited requests (development only)

Production Deployment

For production deployments, each server directory in bear-sunday-servers includes:

  • Dockerfile - Optimized production build
  • docker-compose.prod.yml - Production configuration
  • Health check endpoints
  • OPcache optimization

Example production deployment:

cd swoole  # or roadrunner, frankenphp
docker compose -f docker-compose.prod.yml up -d

Benchmarking

See BEAR.HelloworldBenchmark for benchmark comparisons.

References


Parallel Resource Execution Alpha

BEAR.Async turns the previously sequential fetch of #[Embed] resources into transparent parallel execution. Without touching your resource code, just add a dedicated entrypoint script for parallel execution and embedded resources automatically switch to parallel fetching.

Overview

In standard BEAR.Sunday, #[Embed] resources are fetched sequentially. With BEAR.Async and a runtime selected, they are fetched in parallel.

[Sequential]                     [Parallel]
Request                          Request
    │                                │
    ├── Embed 1 ──── 50ms            ├── Embed 1 ──┬── 50ms
    ├── Embed 2 ──── 50ms            ├── Embed 2 ──┤
    ├── Embed 3 ──── 50ms            ├── Embed 3 ──┤
    └── Embed 4 ──── 50ms            └── Embed 4 ──┘
    │                                │
Response (200ms)                 Response (50ms)

Installation

composer require bear/async

Runtime environments

Choose a runtime that matches your server setup.

Use case Entrypoint Runtime setup
PHP-FPM / Apache (with embedded resources) bin/async.php the library bootstrap.php overlays the parallel runtime on AppModule
Swoole HTTP Server bin/swoole.php install AsyncSwooleModule in AppModule

Parallel execution (ext-parallel)

A runtime for typical web applications running on PHP-FPM / Apache. It executes #[Embed] in parallel using an ext-parallel thread pool.

Add bin/async.php next to bin/app.php. This entrypoint delegates to the library bootstrap.php, which overlays the ext-parallel runtime on top of the normal AppModule.

bin/async.php → vendor/bear/async/bootstrap.php → AppModule + parallel runtime
<?php // bin/async.php

declare(strict_types=1);

require dirname(__DIR__) . '/autoload.php';

$bootstrap = dirname(__DIR__) . '/vendor/bear/async/bootstrap.php';
if (! file_exists($bootstrap)) {
    throw new LogicException('"bear/async" is not installed.');
}

$defaultContext = PHP_SAPI === 'cli' ? 'cli-hal-api-app' : 'hal-api-app';
$context = getenv('APP_CONTEXT') ?: $defaultContext;

exit((require $bootstrap)(
    $context,
    'MyVendor\MyApp',
    dirname(__DIR__),
    $GLOBALS,
    $_SERVER,
));

To change the worker pool size (defaults to the number of CPU cores), pass it explicitly as the 6th argument.

exit((require $bootstrap)($context, 'MyVendor\MyApp', dirname(__DIR__), $GLOBALS, $_SERVER, 8));

ext-parallel constraints

Workers run on separate threads, each with an independent Zend memory space. Embedded resources executed in parallel should be read-only (idempotent GET) resources with no ordering dependency. Because each worker holds its own DI container, request-local mutable state and “same instance” assumptions do not carry across thread boundaries.

Arguments and return values that cross the thread boundary must be copyable: scalar values, null, and nested arrays of those. Passing objects, closures, or resources fails immediately. Keep any interceptors applied to embedded resources executed in parallel idempotent, and do not mutate request-local shared state.

Swoole execution (ext-swoole)

A runtime for applications already running on a Swoole HTTP server and aiming for high concurrency.

Because ext-parallel runs in workers (separate threads), it is selected via a separate entrypoint. ext-swoole, on the other hand, runs inside the same server process, so it is installed as an application module.

use BEAR\Async\Module\AsyncSwooleModule;
use BEAR\Async\Module\PdoPoolEnvModule;

class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new AsyncSwooleModule());
        $this->install(new PdoPoolEnvModule('PDO_DSN', 'PDO_USER', 'PDO_PASSWORD'));
    }
}

In Swoole, coroutines share memory, so a connection pool via PdoPoolEnvModule is required. In read-heavy setups that make heavy use of embedded resources, the pool size should account not only for the number of incoming HTTP requests but also for the number of embeds executed concurrently within one request. To avoid queueing, use PDO_POOL_SIZE >= embed_count * request_concurrency as a starting point; intentionally use a smaller pool when you want to cap concurrent connections to the database.

Technical note (pool connection acquisition): Connection acquisition from the pool is managed per coroutine. Even when both PDO and ExtendedPdo are injected within the same coroutine, they share a single connection and that connection is returned to the pool exactly once via Coroutine::defer() when the coroutine ends. This prevents a single piece of work from unintentionally holding two connections. Furthermore, requests embedded via #[Embed] are lazily evaluated, so the pool is not touched at the point the embed is declared with #[Embed]; connection acquisition is deferred until each request is actually executed.

Technical note (PDOProxy handling): Swoole wraps PDO in its own PDOProxy for coroutine support, but BEAR.Async absorbs this wrapping internally so the value can be treated as a regular PDO. If the original PDO cannot be extracted for some reason, the reflection failure is not propagated as-is; instead it is surfaced as a domain-specific PDO proxy extraction exception.

Swoole coroutines and an active Xdebug do not run safely together. Run Swoole entrypoints with a PHP that does not load Xdebug, or set XDEBUG_MODE=off for local verification.

Usage

Once a runtime is selected, existing #[Embed] resources are automatically executed in parallel.

class Dashboard extends ResourceObject
{
    #[Embed(rel: 'user', src: '/user{?id}')]
    #[Embed(rel: 'notifications', src: '/notifications{?user_id}')]
    #[Embed(rel: 'stats', src: '/stats{?user_id}')]
    public function onGet(string $id): static
    {
        $this->body['id'] = $id;
        return $this;
    }
}

In development, run synchronously via bin/app.php for debugging; in production, switch to parallel execution by starting from bin/async.php.

Why no code change is needed

In BEAR.Sunday, information is structured as resources identified by URIs. #[Embed] does not embed the result of a resource — it embeds the resource request itself and declares a relationship between resources. Choosing the execution strategy — sequential, ext-parallel workers, or Swoole coroutines — is the Linker’s job; resource classes do not need to know whether they were called synchronously or in parallel.

In the default mode these requests are resolved one by one at rendering time. In parallel mode, the moment the first embedded request is resolved, the remaining embedded requests are executed together in parallel. BEAR.Async asynchronous requests share the same type as ordinary BEAR.Resource requests, so the HAL renderer and other surrounding machinery can integrate them into serialization without being aware of the difference.

The “function coloring” problem often raised in async programming — a function calling an async function must itself be async, contaminating the whole codebase — is cut off at the resource boundary. The code is the same under sync and parallel execution; only the execution strategy changes.

This is not specific to BEAR.Async; it is a property of BEAR.Sunday as a whole. Where MVC frameworks write how to execute procedurally, BEAR.Sunday expresses relationships between resources declaratively. Because the declaration is independent of the execution strategy, swapping strategies has no effect on the code.

Demo and benchmarks

The BEAR.Async repository includes a Docker-based demo and benchmark scripts that compare Sync, ext-parallel, and Swoole behavior. See the demo guide and benchmark results for details.

Requirements

Each runtime requires the corresponding PHP extension.

Runtime Requires Application-side change
ext-parallel ZTS PHP + ext-parallel add bin/async.php
ext-swoole ext-swoole install AsyncSwooleModule, use bin/swoole.php

SQL resources with BDR + #[Embed]

To run multiple SQL queries for one page, split each query into its own ResourceObject and let #[Embed] parallelize them via AsyncLinker. The call site just composes resources — the runtime decides how to execute the embeds in parallel.

Combined with Ray.MediaQuery’s BDR pattern (#[DbQuery] interface + factory + immutable domain object), SQL stays in var/sql/*.sql, the call site reads as plain objects, and the resource graph itself is what gets parallelized.

Recipe dependency (not bundled with BEAR.Async):

composer require ray/media-query
use BEAR\Resource\Annotation\Embed;
use BEAR\Resource\ResourceObject;
use Ray\MediaQuery\Annotation\DbQuery;

// Domain object — immutable snapshot
final class UserAccount
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
    ) {
    }
}

// Repository — SQL lives in var/sql/user.sql.
// UserFactory hydrates the row into UserAccount; see BDR_PATTERN.md for factory details.
interface UserRepositoryInterface
{
    #[DbQuery('user', factory: UserFactory::class)]
    public function getUser(int $id): UserAccount;
}

// Resource — one resource per SQL
class User extends ResourceObject
{
    public function __construct(private UserRepositoryInterface $repo)
    {
    }

    public function onGet(int $id): static
    {
        $this->body = ['user' => $this->repo->getUser($id)];

        return $this;
    }
}

// Aggregate — Embeds parallelize automatically under AsyncLinker
class UserDashboard extends ResourceObject
{
    #[Embed(rel: 'user',     src: 'app://self/user{?id}')]
    #[Embed(rel: 'posts',    src: 'app://self/user/posts{?id}')]
    #[Embed(rel: 'comments', src: 'app://self/user/comments{?id}')]
    public function onGet(int $id): static
    {
        return $this;
    }
}
  • SQL stays in var/sql/*.sql (Ray.MediaQuery convention)
  • Domain objects are immutable snapshots; no $results['user'][0] ?? null plumbing at the call site
  • AsyncLinker runs the three embeds in parallel via ext-parallel (PHP-FPM / Apache) or Swoole coroutines
  • Without ext-parallel and without Swoole the same code runs synchronously per request, which is fine for PHP-FPM (each request is its own process)
  • For Swoole, install PdoPoolEnvModule so each coroutine borrows a pooled PDO connection

References


Coding Guide

Project

Vendor should be the company name, team name or the owner (excite, koriym etc.). Package is the name of the application or service (blog, news etc.). Projects must be created on a per application basis. Even when you create a Web API and an HTML from different hosts, they are considered one project.

Style

BEAR.Sunday follows the PSR style.

Here is ResourceObject code example.

<?php
namespace Koriym\Blog\Resource\App;

use BEAR\RepositoryModule\Annotation\Cacheable;
use BEAR\Resource\Annotation\Embed;
use BEAR\Resource\Annotation\Link;
use BEAR\Resource\Code;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
use Ray\AuraSqlModule\AuraSqlInject;

#[Cacheable]
class Entry extends ResourceObject
{
    use AuraSqlInject;
    use ResourceInject;

    #[Embed(rel: 'author', src: '/author{?author_id}')]
    public function onGet(string $author_id, string $slug): static
    {
        // ...

        return $this;
    }

    #[Link(rel: 'next_act', href: '/act1')]
    #[Link(rel: 'next_act2', href: '/act2')]
    public function onPost (
        string $tile,
        string $body,
        string $uid,
        string $slug
    ): static {
        // ...
        $this->code = Code::CREATED;

        return $this;
    }
}

A DocBlock comment is optional. A DocBlock contains the method summary in one line. Then followed by the description, which can be a multiple lines. We should also put @params after description if possible.

Resources

See Resource Best Practices for best practices for resources.

Globals

We do not recommend referencing global values in resources or application classes. It is only used with Modules.

  • Do not refer to the value of Superglobal
  • Do not use define
  • Do not create Config class to hold set values.
  • Do not use global object container (service locator) [1], [2]
  • Use Date function and DateTime class. It is not recommended to get the time directly. Inject the time from outside using koriym/now.

Global method calls such as static methods are also not recommended.

The values required by the application code are all injected. The setting files are used for injecting. When you need an external value such as Web API, make a special gateway class for all requests. Then you can mock that special class with DI or AOP.

Classes and object

  • It is not recommended for the child classes to use the parent class methods. Common functions are not shared by inheritance and trait, they are dedicated classes and they are injected and used. Composite from inheritance.
  • A class with only one method should reflect the function to the class name and should set the name of the method to __invoke () so that function access can be made.

Script command

  • It is recommended to end the setup by using the composer setup command. This script includes the necessary database initialization and library checking. If manual operation such as .env setting is required, it is recommended that the procedure be displayed on the screen.
  • It is recommended that all application caches and logs are cleared with composer cleanup command.
  • It is recommended that all executable test (phpinit/phpcs/phpmd ..) are invoked with composer test command.
  • It is recommended an application is deployed with composer deploy command.

Code check

It is recommended to check the codes for each commit with the following commands. These commands can be installed with bear/qatools.

phpcs src tests
phpmd src text ./phpmd.xml
php-cs-fixer fix --config-file=./.php_cs
phpcbf src

Resources

Please also refer to Resouce best practice.

Code

Returns the appropriate status code. Testing is easier, and the correct information can be conveyed to bots and crawlers.

  • 100 Continue Continuation of multiple requests
  • 200 OK
  • 201 Created Resource Creation
  • 202 Accepted queue / batch acceptance
  • 204 If there is no content body
  • 304 Not Modified Not Updated
  • 400 Bad request
  • 401 Unauthorized Authentication required
  • 403 Forbidden ban
  • 404 Not Found
  • 405 Method Not Allowed
  • 503 Service Unavailable Temporary error on server side

In OnPut method, you deal with the resource state with idempotence. For example, resource creation with UUID or update resource state.

OnPatch is implemented when changing the state of a part of a resource.

HTML Form Method

BEAR.Sunday can overwrite methods using the X-HTTP-Method-Override header or _method query at the POST request in the HTML web form, but it is not necessarily recommended . For the Page resource, it is OK to implement policies other than onGet and onPost. [1], [2]

  • It is recommended that resources with links be indicated by #[Link].
  • It is recommended that resources be embedded as a graph of semantic coherence with #[Embed].

DI

  • Do not inject the value itself of the execution context (prod, dev etc). Instead, we inject instances according to the context. The application does not know in which context it is running.
  • Setter injection is not recommended for library code.
  • It is recommended that you override the toConstructor bindings instead and avoid the Provider bindings as much as possible.
  • Avoid binding by Module according to conditions. AvoidConditionalLogicInModules
  • It is not recommended to reference environmental variables since there is no module. Pass it in the constructor.

AOP

  • Do not make interceptor mandatory. We will make the program work even without an interceptor. (For example, if you remove #[Transactional] interceptor, the function of transaction will be lost, but “core concerns” will work without issue.)
  • Prevent the interceptor from injecting dependencies in methods. Values that can only be determined at implementation time are injected into arguments via #[Assisted] injection.
  • If there are multiple interceptors, do not depend on the execution order.
  • If it is an interceptor unconditionally applied to all methods, consider the description in bootstrap.php.

Environment

To make applications testable, it should also work on the console, and not only on the Web.

It is recommended not to include the .env file in the project repository.

Testing

  • Focus on resource testing using resource clients, adding resource representation testing (e.g. HTML) if needed.
  • Hypermedia tests can leave use cases as tests.
  • prod is the context for production. Use of the prod context in tests should be minimal, preferably none.

HTML templates

  • Avoid large loop statements. Consider replacing if statements in loops with Generator.


Types

This page is a placeholder for types documentation.


PHPDoc Utility Types

Utility types are used to manipulate existing types or dynamically generate new types. Using these types enables more flexible and expressive type definitions.

Table of Contents

  1. [key-of](#key-oft)
  2. [value-of](#value-oft)
  3. [properties-of](#properties-oft)
  4. class-string-map<T of Foo, T>
  5. T[K]
  6. Type aliases
  7. Variable templates

key-of

key-of<T> represents the type of all possible keys of type T.

/**
 * @template T of array
 * @param T $data
 * @param key-of<T> $key
 * @return mixed
 */
function getValueByKey(array $data, $key) {
    return $data[$key];
}

// Usage example
$userData = ['id' => 1, 'name' => 'John'];
$name = getValueByKey($userData, 'name'); // OK
$age = getValueByKey($userData, 'age'); // Psalm will warn

value-of

value-of<T> represents the type of all possible values of type T.

/**
 * @template T of array
 * @param T $data
 * @return value-of<T>
 */
function getRandomValue(array $data) {
    return $data[array_rand($data)];
}

// Usage example
$numbers = [1, 2, 3, 4, 5];
$randomNumber = getRandomValue($numbers); // int type

properties-of

properties-of<T> represents the type of all properties of type T.

class User {
    public int $id;
    public string $name;
    public ?string $email;
}

/**
 * @param User $user
 * @param key-of<properties-of<User>> $property
 * @return value-of<properties-of<User>>
 */
function getUserProperty(User $user, string $property) {
    return $user->$property;
}

// Usage example
$user = new User();
$name = getUserProperty($user, 'name'); // string type
$id = getUserProperty($user, 'id'); // int type
$unknown = getUserProperty($user, 'unknown'); // Psalm will warn

class-string-map<T of Foo, T>

class-string-map represents an array with class names as keys and their instances as values.

interface Repository {}
class UserRepository implements Repository {}
class ProductRepository implements Repository {}

/**
 * @template T of Repository
 * @param class-string-map<T, T> $repositories
 * @param class-string<T> $className
 * @return T
 */
function getRepository(array $repositories, string $className) {
    return $repositories[$className];
}

// Usage example
$repositories = [
    UserRepository::class => new UserRepository(),
    ProductRepository::class => new ProductRepository(),
];

$userRepo = getRepository($repositories, UserRepository::class); // UserRepository type

T[K]

T[K] represents indexed access to type T with key K.

/**
 * @template T of array
 * @template K of key-of<T>
 * @param T $data
 * @param K $key
 * @return T[K]
 */
function getTypedValue(array $data, $key) {
    return $data[$key];
}

// Usage example
$config = [
    'database' => ['host' => 'localhost', 'port' => 3306],
    'cache' => ['driver' => 'redis', 'ttl' => 3600]
];

$dbConfig = getTypedValue($config, 'database'); // array{host: string, port: int}
$host = getTypedValue($config['database'], 'host'); // string

Type aliases

Type aliases allow you to create reusable type definitions.

/**
 * @psalm-type UserId = positive-int
 * @psalm-type UserData = array{id: UserId, name: string, email: string}
 * @psalm-type UserCollection = array<UserId, UserData>
 */

class UserService {
    /**
     * @param UserData $userData
     * @return UserId
     */
    public function createUser(array $userData): int {
        // Implementation
        return $userData['id'];
    }
    
    /**
     * @param UserCollection $users
     * @param UserId $id
     * @return UserData|null
     */
    public function findUser(array $users, int $id): ?array {
        return $users[$id] ?? null;
    }
}

Variable templates

Variable templates allow for more dynamic type definitions.

/**
 * @template T
 * @template K of key-of<T>
 * @param T $data
 * @param K ...$keys
 * @return array<K, T[K]>
 */
function pick(array $data, ...$keys): array {
    $result = [];
    foreach ($keys as $key) {
        if (array_key_exists($key, $data)) {
            $result[$key] = $data[$key];
        }
    }
    return $result;
}

// Usage example
$user = [
    'id' => 1,
    'name' => 'John',
    'email' => 'john@example.com',
    'password' => 'secret'
];

$publicData = pick($user, 'id', 'name', 'email'); 
// array{id: int, name: string, email: string}

Advanced Utility Type Examples

Conditional Types

/**
 * @template T
 * @psalm-type NonEmpty<T> = T is array ? non-empty-array<T> : T
 */

/**
 * @template T of array
 * @param T $data
 * @return NonEmpty<T>
 * @throws InvalidArgumentException
 */
function ensureNonEmpty(array $data): array {
    if (empty($data)) {
        throw new InvalidArgumentException('Array cannot be empty');
    }
    return $data;
}

Recursive Types

/**
 * @psalm-type JsonValue = scalar|null|JsonArray|JsonObject
 * @psalm-type JsonArray = array<JsonValue>
 * @psalm-type JsonObject = array<string, JsonValue>
 */

class JsonParser {
    /**
     * @param string $json
     * @return JsonValue
     */
    public function parse(string $json) {
        return json_decode($json, true);
    }
}

Mapped Types

/**
 * @template T of object
 * @psalm-type Partial<T> = array<key-of<properties-of<T>>, value-of<properties-of<T>>|null>
 */

class UserUpdateService {
    /**
     * @param User $user
     * @param Partial<User> $updates
     * @return User
     */
    public function updateUser(User $user, array $updates): User {
        foreach ($updates as $property => $value) {
            if ($value !== null && property_exists($user, $property)) {
                $user->$property = $value;
            }
        }
        return $user;
    }
}

// Usage
$user = new User();
$updates = ['name' => 'Jane', 'email' => null]; // Partial<User>
$updatedUser = $service->updateUser($user, $updates);

Best Practices

1. Use Descriptive Type Names

// Good
/**
 * @psalm-type DatabaseConfig = array{host: string, port: positive-int, database: string}
 */

// Avoid
/**
 * @psalm-type Config = array{host: string, port: positive-int, database: string}
 */

2. Combine Utility Types for Complex Scenarios

/**
 * @template T of object
 * @template K of key-of<properties-of<T>>
 * @param T $object
 * @param K $property
 * @return properties-of<T>[K]
 */
function getProperty(object $object, string $property) {
    return $object->$property;
}

3. Document Complex Type Relationships

/**
 * Repository pattern with typed collections
 * 
 * @template TEntity of object
 * @template TId of scalar
 * @psalm-type EntityCollection<TEntity, TId> = array<TId, TEntity>
 * @psalm-type EntitySpec<TEntity> = array<key-of<properties-of<TEntity>>, mixed>
 */
interface Repository {
    /**
     * @param TId $id
     * @return TEntity|null
     */
    public function find($id): ?object;
    
    /**
     * @param EntitySpec<TEntity> $criteria
     * @return EntityCollection<TEntity, TId>
     */
    public function findBy(array $criteria): array;
}

Integration with Static Analysis Tools

These utility types work best with static analysis tools like Psalm and PHPStan:

Psalm Configuration

<!-- psalm.xml -->
<psalm>
    <projectFiles>
        <directory name="src" />
    </projectFiles>
    <plugins>
        <pluginClass class="Psalm\Plugin\DocblockTypeProvider" />
    </plugins>
</psalm>

PHPStan Configuration

# phpstan.neon
parameters:
    level: 8
    paths:
        - src
    treatPhpDocTypesAsCertain: false

Utility types provide powerful abstractions for type-safe PHP development, enabling more robust code with better IDE support and static analysis capabilities.


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

use BEAR\Resource\ResourceInterface;

class TodoTest extends TestCase
{
    private ResourceInterface $resource;
    
    protected function setUp(): void
    {
        $injector = Injector::getInstance('test-html-app');
        $this->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.

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.

$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.

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

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. 25 After running the SUT containing the spy target, verify the number of calls and the value of the calls in the spy log.

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<int, Log> $addLog
    $addLog = $spyLog->getLogs(FooInterface, 'add');   
    $this->assertSame(1, count($addLog), 'Should have received once');
    // Argument validation from SUT
    $this->assertSame([1, 2], $addLog[0]->arguments);
    $this->assertSame(1, $addLog[0]->namedArguments['a']);
}

Dummy

Use Null Binding to bind a null object to an interface.

Hypermedia Test

Resource testing is an input/output test for each endpoint. Hypermedia tests, on the other hand, test the workflow behavior of how the endpoints are connected.

Workflow tests are inherited from HTTP tests and are tested at both the PHP and HTTP levels in a single code. HTTP testing is done with curl and the request/response is logged in a log file.

Best Practice

  • Test the interface, not the implementation.
  • Create a actual fake class rather than using a mock library.
  • Testing is a specification. Ease of reading rather than ease of coding.

Reference


Security Beta

Security tools can scan your application for vulnerability assessment. With static analysis, dynamic testing, taint analysis, and AI auditing, architecture-aware tools analyze from multiple angles, detecting vulnerabilities that generic tools miss.

Installation

Install bear/security.

composer require --dev bear/security

Scanning Tools

Tool What it does When to use
SAST26 Static analysis to find dangerous patterns in your code During development
DAST27 Dynamic analysis to send attack requests to your app Before deployment
AI Auditor AI reviews your code for security issues Code review
Psalm Plugin Traces user input to dangerous operations During development

Design Philosophy: Recall-First

Security scanners have traditionally had two approaches: precision-first (report only certain issues) and recall-first (report suspicious patterns), with a trade-off between them.

BEAR.Security adopts a recall-first approach. Missing a vulnerability (False Negative) is critical, but false positives (False Positive) can be reviewed and excluded. With AI agents now able to handle false positive verification, this strategy is more effective than ever.

# 1. Run SAST to detect pattern-based vulnerabilities
./vendor/bin/bear.security-scan src

# 2. Review results and fix vulnerabilities
# Add @security-ignore comment to false positives (see example below)

# 3. Run AI Auditor to detect business logic issues
./vendor/bin/bear-security-audit src

# 4. Review and fix detected issues

Example of suppressing a false positive:

$path = $this->buildPath($id); // @security-ignore PATH_TRAVERSAL_FILE_OPS: $id is validated integer from router

Once @security-ignore is added, the issue is suppressed in subsequent scans.

SAST

Scans your source code for dangerous patterns. We recommend running this from an AI agent (such as Claude Code) and having the AI verify whether detections are false positives.

./vendor/bin/bear.security-scan src

Detects 14 vulnerability types:

Category Examples
Injection SQL injection, Command injection, XSS
Access Control Path traversal, Open redirect
Cryptography Weak hash algorithms, Hardcoded secrets
Data Protection Insecure deserialization, XXE
Session Session fixation, CSRF
Network SSRF, Remote file inclusion

See the Vulnerability Reference for details on each vulnerability.

DAST

Sends attack payloads to your running application to test real vulnerabilities:

./vendor/bin/bear-security-dast 'MyVendor\MyApp' prod-app /path/to/app

Tests include:

Test What it sends
SQL Injection ' OR '1'='1, ; DROP TABLE
XSS <script>alert(1)</script>
Command Injection ; ls -la, \| cat /etc/passwd
Path Traversal ../../../etc/passwd
Security Headers Checks for missing headers

AI Auditor

Uses Claude AI to find security issues that pattern matching cannot detect:

# Option 1: API Key
export ANTHROPIC_API_KEY=sk-ant-...
./vendor/bin/bear-security-audit src

# Option 2: Claude CLI (Max Plan - no API key required)
claude auth login
./vendor/bin/bear-security-audit src
Issue Description
IDOR Accessing other users’ data without authorization check
Mass Assignment Accepting unvalidated fields in updates
Race Condition Time-of-check to time-of-use flaws
Business Logic Application-specific security flaws

Psalm Plugin (Taint Analysis)

Taint analysis is a static analysis technique that marks user input as tainted variables and traces how that taint propagates through your code. It reports vulnerabilities when tainted data reaches SQL queries or HTML output without proper sanitization.

Setup

Add the plugin and stubs to your psalm.xml:

<?xml version="1.0"?>
<psalm
    xmlns="https://getpsalm.org/schema/config"
    errorLevel="1"
>
    <projectFiles>
        <directory name="src"/>
    </projectFiles>
    <stubs>
        <file name="vendor/bear/security/stubs/AuraSql.phpstub"/>
        <file name="vendor/bear/security/stubs/PDO.phpstub"/>
        <file name="vendor/bear/security/stubs/Qiq.phpstub"/>
    </stubs>
    <plugins>
        <pluginClass class="BEAR\Security\Psalm\ResourceTaintPlugin">
            <targets>
                <target>Page</target>
                <target>App</target>
            </targets>
        </pluginClass>
    </plugins>
</psalm>

The targets specify which resources receive external input. Use Page when serving web pages with html context, App when serving APIs with api context.

Stubs

Stubs provide taint annotations for third-party libraries:

Stub Purpose
AuraSql.phpstub Marks SQL query methods as taint sinks
PDO.phpstub Marks PDO methods as taint sinks
Qiq.phpstub Marks template output as taint sinks

Running

Run taint analysis:

./vendor/bin/psalm --taint-analysis

Add convenience scripts to composer.json:

{
    "scripts": {
        "security": "./vendor/bin/bear.security-scan src",
        "taint": "./vendor/bin/psalm --taint-analysis 2>&1 | grep -E 'Tainted' || true"
    },
    "scripts-descriptions": {
        "security": "Run SAST security scan",
        "taint": "Run Psalm taint analysis"
    }
}

Then run with:

composer security
composer taint

GitHub Actions

You can add security scanning to your CI pipeline:

cp vendor/bear/security/workflows/security-sast.yml .github/workflows/

This workflow runs on every push and pull request:

Job What it does
SAST Scan Scans code and uploads results to GitHub Security tab
Psalm Taint Traces user input flows and uploads results to GitHub Security tab

Results appear in your repository’s Security > Code scanning section.

Recommended: Run the scan from an AI agent first, add @security-ignore to false positives, then enable CI.

Architecture and Security

BEAR.Sunday’s architecture makes security scanning more effective:

  • Clear Entry Points: Every endpoint is a ResourceObject with onGet, onPost methods. Scanners can identify all inputs and trace data flow.

  • No Hidden Magic: Dependencies are explicit through constructor injection. Scanners can analyze the complete code path.

  • Framework-Aware AI: The AI Auditor understands BEAR.Sunday patterns and can detect business logic flaws, not just generic vulnerabilities.

Prompt for AI Agents

To set up bear/security with an AI coding assistant, use this prompt:

Follow the setup instructions at:
https://raw.githubusercontent.com/bearsunday/BEAR.Skills/1.x/.claude/skills/bear-security-setup/SKILL.md

Examples

This example application is built on the principles described in the Coding Guide.

Polidog.Todo

https://github.com/koriym/Polidog.Todo

Todos is a basic CRUD application. The DB is accessed using the static SQL file in the var/sql directory. Includes REST API using hyperlinks and testing, as well as form validation tests.

MyVendor.ContactForm

https://github.com/bearsunday/MyVendor.ContactForm

It is a sample of various form pages.

  • Minimal form page
  • Multiple forms page
  • Looped input form page
  • Preview form page including checkbox and radio button

Attributes

BEAR.Sunday supports PHP8’s attributes in addition to the annotations.

Annotation

/**
 * @Inject
 * @Named('admin')
 */
public function setLogger(LoggerInterface $logger)

Attribute

#[Inject, Named('admin')]
public function setLogger(LoggerInterface $logger)
#[Embed(rel: 'weather', src: 'app://self/weather{?date}')]
#[Link(rel: 'event', href: 'app://self/event{?news_date}')]
public function onGet(string $date): self

Apply to parameters

While some annotations can only be applied to methods and require the argument names to be specified by name, the Attributes can be used to decorate arguments directly.

public __construct(#[Named('payment')] LoggerInterface $paymentLogger, #[Named('debug')] LoggerInterface $debugLogger)
public function onGet($id, #[Assisted] DbInterface $db = null)
public function onGet(#[CookieParam('id')]string $tokenId): void
public function onGet(#[ResourceParam(uri: 'app://self/login#nickname')] string $nickname = null): static

Compatibility

Attributes and annotations can be mixed in a single project. 2 All annotations described in this manual will work when converted to attributes.

Performance

Although the cost of loading annotations/attributes for production is minimal due to optimization, you can speed up development by declaring that you will only use attribute readers, as follows

// tests/bootstap.php 

use Ray\ServiceLocator\ServiceLocator;

ServiceLocator::setReader(new AttributeReader());
// DevModule
 
$this->install(new AttributeModule());


API Doc

Your application is the documentation.

  • ApiDoc HTML: Developer documentation
  • OpenAPI 3.1: Tool chain integration
  • JSON Schema: Information model
  • ALPS: Vocabulary semantics for AI understanding
  • llms.txt: AI-readable application overview

Demo

Installation

composer require bear/api-doc --dev
./vendor/bin/apidoc init

The init command generates apidoc.xml from your composer.json. Edit it to customize.

<apidoc>
    <appName>MyVendor\MyProject</appName>  <!-- Application namespace -->
    <scheme>app</scheme>                    <!-- app or page -->
    <docDir>docs/api</docDir>
    <format>html</format>                   <!-- html, openapi, etc. -->
</apidoc>

The format accepts comma-separated values: html, md, openapi, llms.

Usage

Generate documentation from the command line.

./vendor/bin/apidoc

OpenAPI HTML Generation

When openapi format is specified, openapi.json is generated. Use Redocly CLI to convert it to HTML.

npm install -g @redocly/cli
redocly build-docs docs/api/openapi.json -o docs/api/openapi.html

llms.txt

The llms format generates llms.txt following the llms.txt specification. The output includes API endpoints, resource objects, infrastructure interfaces (Query/Command), SQL statements, and entity definitions.

Composer Scripts

Add scripts to composer.json for convenience.

{
    "scripts": {
        "docs": "./vendor/bin/apidoc"
    },
    "scripts-descriptions": {
        "docs": "Generate API documentation"
    }
}
composer docs

GitHub Actions

Push to main branch to automatically generate and publish API documentation to GitHub Pages. The reusable workflow handles HTML generation, OpenAPI conversion with Redocly, and ALPS state diagram creation.

name: API Docs
on:
  push:
    branches: [main]

jobs:
  docs:
    uses: bearsunday/BEAR.ApiDoc/.github/workflows/apidoc.yml@v1
    with:
      format: 'html,openapi,llms'

Enable GitHub Pages: Settings → Pages → Source: “GitHub Actions”

Inputs

Input Default Description
php-version '8.2' PHP version
format 'html,openapi,llms' Comma-separated: html, md, openapi, llms
docs-path 'docs' Output directory
publish-to 'github-pages' github-pages or artifact-only

Output Structure

docs/
├── index.html          # API documentation
├── llms.txt            # AI-readable overview
├── openapi.json        # OpenAPI spec
└── schemas/
    ├── index.html      # Schema list
    └── *.json          # JSON Schema

Configuration

<?xml version="1.0" encoding="UTF-8"?>
<apidoc
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://bearsunday.github.io/BEAR.ApiDoc/apidoc.xsd">
    <appName>MyVendor\MyProject</appName>
    <scheme>app</scheme>
    <docDir>docs</docDir>
    <format>html</format>
    <alps>alps.json</alps>
</apidoc>
Option Required Description
appName Yes Application namespace
scheme Yes app or page
docDir Yes Output directory
format Yes html, md, openapi, llms
title   API title
alps   ALPS profile path

Profile

ALPS profile defines your API vocabulary. Centralized definitions prevent inconsistencies and aid shared understanding.

{
    "$schema": "https://alps-io.github.io/schemas/alps.json",
    "alps": {
        "descriptor": [
            {"id": "firstName", "title": "The person's first name."},
            {"id": "familyName", "def": "https://schema.org/familyName"}
        ]
    }
}

Application as Documentation

Code is the single source of truth. Documentation generated from your application never diverges from the implementation.

llms.txt provides AI-readable application overviews. When an AI agent encounters your application, it can quickly grasp the entire structure through this single document—generated directly from your code. Unlike typical API references that list endpoints, llms.txt captures the full information architecture following Dan Klyn’s framework—Ontology, Taxonomy, and Choreography. Combined with ALPS vocabulary semantics and JSON Schema information models, AI agents can understand not just what operations exist, but the meaning and structure behind them.

Reference


Reference

Attributes

Attribute Description
#[CacheableResponse] An attribute to indicate a cacheable response.
#[Cacheable(int $expirySecond = 0)] An attribute to indicate the cacheability of a resource. $expirySecond is the cache expiration time in seconds.
#[CookieParam(string $name)] An attribute to receive parameters from cookies. $name is the name of the cookie.
#[DonutCache] An attribute to indicate Donut cache.
#[Embed(src: string $src, rel: string $rel)] An attribute to indicate embedding other resources. $src is the URI of the embedded resource, $rel is the relation name.
#[EnvParam(string $name)] An attribute to receive parameters from environment variables. $name is the name of the environment variable.
#[FormParam(string $name)] An attribute to receive parameters from form data. $name is the name of the form field.
#[Inject] An attribute to indicate setter injection.
#[InputValidation] An attribute to indicate input validation.
#[JsonSchema(key: string $key = null, schema: string $schema = null, params: string $params = null)] An attribute to specify the JSON schema for input/output of a resource. $key is the schema key, $schema is the schema file name, $params is the schema file name for parameters.
#[Link(rel: string $rel, href: string $href, method: string $method = null)] An attribute to indicate links between resources. $rel is the relation name, $href is the URI of the linked resource, $method is the HTTP method.
#[Named(string $name)] An attribute to indicate named binding. $name is the binding name.
#[OnFailure(string $name = null)] An attribute to specify a method for validation failure. $name is the name of the validation.
#[OnValidate(string $name = null)] An attribute to specify a validation method. $name is the name of the validation.
#[Produces(array $mediaTypes)] An attribute to specify the output media types of a resource. $mediaTypes is an array of producible media types.
#[QueryParam(string $name)] An attribute to receive query parameters. $name is the name of the query parameter.
#[RefreshCache] An attribute to indicate cache refresh.
#[ResourceParam(uri: string $uri, param: string $param)] An attribute to receive the result of another resource as a parameter. $uri is the URI of the resource, $param is the parameter name.
#[ReturnCreatedResource] An attribute to indicate that the created resource will be returned.
#[ServerParam(string $name)] An attribute to receive parameters from server variables. $name is the name of the server variable.
#[Ssr(app: string $appName, state: array $state = [], metas: array $metas = [])] An attribute to indicate server-side rendering. $appName is the name of the JS application, $state is the state of the application, $metas is an array of meta information.
#[Transactional(array $props = ['pdo'])] An attribute to indicate that a method will be executed within a transaction. $props is an array of properties to which the transaction will be applied.
#[UploadFiles] An attribute to receive uploaded files.
#[Valid(form: string $form = null, onFailure: string $onFailure = null)] An attribute to indicate request validation. $form is the form class name, $onFailure is the method name for validation failure.

Modules

Module Name Description
ApcSsrModule A module for server-side rendering using APCu.
ApiDoc A module for generating API documentation.
AppModule The main module of the application. It installs and configures other modules.
AuraSqlModule A module for database connection using Aura.Sql.
AuraSqlQueryModule A module for query builder using Aura.SqlQuery.
CacheVersionModule A module for cache version management.
CliModule A module for command-line interface.
DoctrineOrmModule A module for database connection using Doctrine ORM.
FakeModule A fake module for testing purposes.
HalModule A module for HAL (Hypertext Application Language).
HtmlModule A module for HTML rendering.
ImportAppModule A module for loading other applications.
JsonSchemaModule A module for input/output validation of resources using JSON schema.
JwtAuthModule A module for authentication using JSON Web Token (JWT).
NamedPdoModule A module that provides named PDO instances.
PackageModule A module that installs the basic modules provided by BEAR.Package together.
ProdModule A module for production environment settings.
QiqModule A module for the Qiq template engine.
ResourceModule A module for settings related to resource classes.
AuraRouterModule A module for routing using Aura.Router.
SirenModule A module for Siren (Hypermedia Specification).
SpyModule A module for recording method calls.
SsrModule A module for server-side rendering.
TwigModule A module for the Twig template engine.
ValidationModule A module for validation.
  1. See parse_str  2

  2. Attributes take precedence when mixed in a single method.  2 3

  3. This SQL conforms to the SQL Style Guide. It can be configured from PhpStorm as Joe Celko

  4. PHPStorm Database Tools and SQL 

  5. Database Diagrams, etc. to check the query plan and execution plan to improve the quality of the SQL you create. 

  6. PHP 8.0+ named arguments ¶, column order for PHP 7.x. 

  7. MediaQuery README Here we run it directly from mysql as an example, but you should also learn how to enter seed in the migration tool and use the IDE’s DB tools. 

  8. Ray.MediaQuery also supports HTTP API requests. 

  9. MediaQuery also supports HTTP API requests. This hierarchical structure of content is called Taxonomy in IA (Information Architecture). See Understanding Information Architecture

  10. Ray.MediaQuery README 

  11. It is a widespread misconception that the Uniform Interface is not an HTTP method. See Uniform Interface

  12. The so-called “Restish API”; many APIs introduced as REST APIs have this URI/object style, and REST is misused. 

  13. If you remove the links from the tutorial, you get the URI style. 

  14. https://www.iana.org/assignments/media-types/media-types.xhtml 

  15. Dependency inversion principle 

  16. 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. 

  17. Called with named arguments in PHP8.x, but with positional arguments in PHP7.x. 

  18. When sending API requests as JSON, set the content-type header to application/json

  19. out-bound links e.g.) html can link to other related html. 

  20. embedded links Example: html can embed independent image resources. 

  21. This is similar to an object graph where the dependency tree is a graph in DI. 

  22. query-locater is a library for handling SQL as files, which is useful with Aura.Sql. 

  23. The mechanism is similar to Java’s DB access framework Doma

  24. The name is derived from a similar feature in the Seaside framework for Smalltalk. 

  25. ray/test-double must be installed to use SpyModule. 

  26. Static Application Security Testing 

  27. Dynamic Application Security Testing