{"id":26420192,"url":"https://github.com/ody-dev/framework","last_synced_at":"2025-03-18T02:03:14.093Z","repository":{"id":281825185,"uuid":"946543744","full_name":"ody-dev/framework","owner":"ody-dev","description":"ODY's RESTful API framework built on top of Swoole","archived":false,"fork":false,"pushed_at":"2025-03-16T15:33:20.000Z","size":376,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-16T15:45:56.781Z","etag":null,"topics":["api","microservices","php","rest-api","restful-api","swoole"],"latest_commit_sha":null,"homepage":"https://ody.dev","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ody-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-11T09:59:43.000Z","updated_at":"2025-03-16T15:33:24.000Z","dependencies_parsed_at":"2025-03-11T11:21:48.445Z","dependency_job_id":"3bb6402f-b30e-48f3-a238-acd2d0c22f6b","html_url":"https://github.com/ody-dev/framework","commit_stats":null,"previous_names":["ilyasdeckers/rest-api","ody-dev/framework"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ody-dev%2Fframework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ody-dev%2Fframework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ody-dev%2Fframework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ody-dev%2Fframework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ody-dev","download_url":"https://codeload.github.com/ody-dev/framework/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244141539,"owners_count":20404836,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","microservices","php","rest-api","restful-api","swoole"],"created_at":"2025-03-18T02:01:05.620Z","updated_at":"2025-03-18T02:03:14.078Z","avatar_url":"https://github.com/ody-dev.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ODY Framework Documentation\n\n\u003e [!WARNING]\n\u003e 🚧 !! This is a WIP, build completely from scratch. This will replace ody-core which was build on top of Slim framework. !!\n\n## Introduction\n\nODY is a modern PHP API framework built with a focus on high performance and modern architecture. It leverages \nSwoole's coroutines for asynchronous processing, follows PSR standards for interoperability, and provides a \nclean architecture for building robust APIs.\n\n## Installation\n### Requirements\n\n- PHP 8.3 or higher\n- Swoole PHP extension (≥ 6.0.0)\n- Composer\n\n### Basic Installation\n\n```bash\ncomposer create-project ody/framework your-project-name\ncd your-project-name\n```\n\n## Configuration\n\nConfiguration files are stored in the `config` directory. The primary configuration files include:\n\n- `app.php`: Application settings, service providers, and middleware\n- `database.php`: Database connections configuration\n- `logging.php`: Logging configuration and channels\n\nEnvironment-specific configurations can be set in `.env` files. A sample `.env.example` file is provided that you can copy to `.env` and customize:\n\n```bash\ncp .env.example .env\n```\n\n## Project Structure\n\n```\nyour-project/\n├── app/                  # Application code\n│   ├── Controllers/      # Controller classes\n│   └── ...\n├── config/               # Configuration files\n├── public/               # Public directory (web server root)\n│   └── index.php         # Application entry point\n├── routes/               # Route definitions\n│   └── api.php           # API routes\n├── src/                  # Framework core components\n├── storage/              # Storage directory for logs, cache, etc.\n├── tests/                # Test files\n├── vendor/               # Composer dependencies\n├── .env                  # Environment variables\n├── .env.example          # Environment variables example\n├── composer.json         # Composer package file\n├── ody                   # CLI entry point\n└── README.md             # Project documentation\n```\n\n## Routing\n\nRoutes are defined in the `routes` directory. The framework supports various HTTP methods and route patterns:\n\n```php\n// Basic route definition\nRoute::get('/hello', function (ServerRequestInterface $request, ResponseInterface $response) {\n    return $response-\u003ejson([\n        'message' =\u003e 'Hello World'\n    ]);\n});\n\n// Route with named controller\nRoute::post('/users', 'App\\Controllers\\UserController@store');\n\n// Route with middleware\nRoute::get('/users/{id}', 'App\\Controllers\\UserController@show')\n    -\u003emiddleware('auth');\n\n// Route groups\nRoute::group(['prefix' =\u003e '/api/v1', 'middleware' =\u003e ['throttle:60,1']], function ($router) {\n    $router-\u003eget('/status', function ($request, $response) {\n        return $response-\u003ejson([\n            'status' =\u003e 'operational'\n        ]);\n    });\n});\n```\n\n## Controllers\n\nControllers handle the application logic and are typically stored in the `app/Controllers` directory:\n\n```php\n\u003c?php\n\nnamespace App\\Controllers;\n\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass UserController\n{\n    private $logger;\n    \n    public function __construct(LoggerInterface $logger)\n    {\n        $this-\u003elogger = $logger;\n    }\n    \n    public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface\n    {\n        // Get all users\n        $users = [\n            ['id' =\u003e 1, 'name' =\u003e 'John Doe'],\n            ['id' =\u003e 2, 'name' =\u003e 'Jane Smith']\n        ];\n        \n        return $response-\u003ewithHeader('Content-Type', 'application/json')\n                       -\u003ewithBody(json_encode($users));\n    }\n    \n    public function show(ServerRequestInterface $request, ResponseInterface $response, array $params): ResponseInterface\n    {\n        $id = $params['id'];\n        \n        // Get user by ID\n        $user = ['id' =\u003e $id, 'name' =\u003e 'John Doe'];\n        \n        return $response-\u003ewithHeader('Content-Type', 'application/json')\n                       -\u003ewithBody(json_encode($user));\n    }\n}\n```\n\n# Middleware\n\nMiddleware provides a mechanism for filtering and modifying HTTP requests and responses. The ODY Framework implements\nthe PSR-15 middleware standard, allowing for a consistent approach to handling request processing.\n\nKey features:\n- PSR-15 compliant implementation\n- Support for named middleware\n- Middleware grouping\n\n## Using Built-in Middleware\n\n### Registering Middleware in Routes\n\nYou can apply middleware to routes using the `middleware()` method:\n\n```php\n// Apply a single middleware\n$router-\u003eget('/profile', 'UserController@profile')\n    -\u003emiddleware('auth');\n\n// Apply multiple middleware\n$router-\u003eget('/admin/dashboard', 'AdminController@dashboard')\n    -\u003emiddleware('auth', 'role:admin');\n```\n\n### Global Middleware\n\nGlobal middleware runs on every request. Configure it in your `app.php` configuration file:\n\n```php\n'middleware' =\u003e [\n    // Global middleware applied to all routes\n    'global' =\u003e [\n        Ody\\Foundation\\Middleware\\ErrorHandlerMiddleware::class,\n        Ody\\Foundation\\Middleware\\CorsMiddleware::class,\n        Ody\\Foundation\\Middleware\\JsonBodyParserMiddleware::class,\n    ],\n]\n```\n\n### Named Middleware\n\nNamed middleware allows you to reference middleware by a short name. Define named middleware in your `app.php` configuration:\n\n```php\n'middleware' =\u003e [\n    // Named middleware that can be referenced in routes\n    'named' =\u003e [\n        'auth' =\u003e Ody\\Foundation\\Middleware\\AuthMiddleware::class,\n        'role' =\u003e Ody\\Foundation\\Middleware\\RoleMiddleware::class,\n        'throttle' =\u003e Ody\\Foundation\\Middleware\\ThrottleMiddleware::class,\n        'cors' =\u003e Ody\\Foundation\\Middleware\\CorsMiddleware::class,\n        'json' =\u003e Ody\\Foundation\\Middleware\\JsonBodyParserMiddleware::class,\n    ],\n]\n```\n\n## Middleware Groups\n\nMiddleware groups allow you to apply multiple middleware with a single reference. Define groups in your `app.php` configuration:\n\n```php\n'middleware' =\u003e [\n    // Middleware groups for route groups\n    'groups' =\u003e [\n        'web' =\u003e [\n            'auth',\n            'json',\n        ],\n        'api' =\u003e [\n            'throttle',\n            'auth',\n            'json',\n        ],\n    ],\n]\n```\n\nApply a middleware group to a route:\n\n```php\n$router-\u003egroup(['middleware' =\u003e 'api'], function ($router) {\n    $router-\u003eget('/users', 'UserController@index');\n    $router-\u003epost('/users', 'UserController@store');\n});\n```\n\n## Creating Custom Middleware\n\n### Basic Middleware\n\nCreating a custom middleware requires implementing the PSR-15 `MiddlewareInterface`:\n\n```php\n\u003c?php\n\nnamespace App\\Http\\Middleware;\n\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\Http\\Server\\MiddlewareInterface;\nuse Psr\\Http\\Server\\RequestHandlerInterface;\n\nclass CustomMiddleware implements MiddlewareInterface\n{\n    /**\n     * Process an incoming server request\n     *\n     * @param ServerRequestInterface $request\n     * @param RequestHandlerInterface $handler\n     * @return ResponseInterface\n     */\n    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface\n    {\n        // Your logic before passing the request to the next middleware\n\n        // Process the request with the next middleware or route handler\n        $response = $handler-\u003ehandle($request);\n\n        // Your logic after receiving the response from the next middleware\n\n        return $response;\n    }\n}\n```\n\n### Registering Custom Middleware\n\nRegister your custom middleware in the `app.php` configuration:\n\n```php\n'middleware' =\u003e [\n    'named' =\u003e [\n        'custom' =\u003e App\\Http\\Middleware\\CustomMiddleware::class,\n        'custom-param' =\u003e App\\Http\\Middleware\\CustomParameterizedMiddleware::class,\n    ],\n]\n```\n\nNow you can use your custom middleware in routes:\n\n```php\n$router-\u003eget('/custom-route', 'Controller@method')\n    -\u003emiddleware('custom', 'custom-param:special');\n```\n\n## Advanced Usage\n\n### Middleware Priority\n\nMiddleware executes in the order they are registered. Global middleware runs first, followed by group middleware, and finally route-specific middleware.\n\n### Stopping Middleware Execution\n\nTo stop the middleware chain and return a response early:\n\n```php\npublic function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface\n{\n    if ($someCondition) {\n        // Return response without calling $handler-\u003ehandle($request)\n        return new Response()\n            -\u003ewithStatus(403)\n            -\u003ejson()\n            -\u003ewithJson(['error' =\u003e 'Access denied']);\n    }\n    \n    return $handler-\u003ehandle($request);\n}\n```\n\n### Modifying the Request\n\nYou can modify the request before passing it to the next middleware:\n\n```php\npublic function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface\n{\n    // Add data to the request\n    $request = $request-\u003ewithAttribute('custom_data', 'value');\n    \n    return $handler-\u003ehandle($request);\n}\n```\n\n### Modifying the Response\n\nYou can also modify the response after receiving it from the next middleware:\n\n```php\npublic function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface\n{\n    // Process the request\n    $response = $handler-\u003ehandle($request);\n    \n    // Modify the response\n    return $response-\u003ewithHeader('X-Custom-Header', 'value');\n}\n```\n\n## Dependency Injection\n\n```php\n\u003c?php\n\n// Binding an interface to a concrete implementation\n$container-\u003ebind(UserRepositoryInterface::class, UserRepository::class);\n\n// Registering a singleton\n$container-\u003esingleton(UserService::class, function($container) {\n    return new UserService(\n        $container-\u003emake(UserRepositoryInterface::class)\n    );\n});\n\n// Resolving dependencies\n$userService = $container-\u003emake(UserService::class);\n```\n\n## Logging\n\nLogging is configured in `config/logging.php` and provides various channels for logging:\n\n```php\n// Using the logger\n$logger-\u003einfo('User logged in', ['id' =\u003e $userId]);\n$logger-\u003eerror('Failed to process payment', ['order_id' =\u003e $orderId]);\n\n// Using the logger helper function\nlogger()-\u003einfo('Processing request');\n```\n\n# Custom Loggers in ODY Framework\n## Creating Custom Loggers\n\n### Basic Requirements\n\nAll custom loggers must:\n\n1. Extend `Ody\\Logger\\AbstractLogger`\n2. Implement a static `create(array $config)` method\n3. Override the `write(string $level, string $message, array $context = [])` method\n\n### Example: Creating a Custom Logger\n\nHere's a simple example of a custom logger that logs to Redis:\n\n```php\n\u003c?php\n\nnamespace App\\Logging;\n\nuse Ody\\Logger\\AbstractLogger;\nuse Ody\\Logger\\FormatterInterface;\nuse Ody\\Logger\\JsonFormatter;\nuse Ody\\Logger\\LineFormatter;\nuse Psr\\Log\\LoggerInterface;\nuse Psr\\Log\\LogLevel;\nuse Redis;\n\nclass RedisLogger extends AbstractLogger\n{\n    /**\n     * @var Redis\n     */\n    protected Redis $redis;\n    \n    /**\n     * @var string\n     */\n    protected string $channel;\n    \n    /**\n     * Constructor\n     */\n    public function __construct(\n        Redis $redis,\n        string $channel = 'logs',\n        string $level = LogLevel::DEBUG,\n        ?FormatterInterface $formatter = null\n    ) {\n        parent::__construct($level, $formatter);\n        $this-\u003eredis = $redis;\n        $this-\u003echannel = $channel;\n    }\n    \n    /**\n     * Create a Redis logger from configuration\n     */\n    public static function create(array $config): LoggerInterface\n    {\n        // Create Redis connection\n        $redis = new Redis();\n        $redis-\u003econnect(\n            $config['host'] ?? '127.0.0.1',\n            $config['port'] ?? 6379\n        );\n        \n        if (isset($config['password'])) {\n            $redis-\u003eauth($config['password']);\n        }\n        \n        // Create formatter\n        $formatter = null;\n        if (isset($config['formatter'])) {\n            $formatter = self::createFormatter($config);\n        }\n        \n        // Return new logger instance\n        return new self(\n            $redis,\n            $config['channel'] ?? 'logs',\n            $config['level'] ?? LogLevel::DEBUG,\n            $formatter\n        );\n    }\n    \n    /**\n     * Create a formatter based on configuration\n     */\n    protected static function createFormatter(array $config): FormatterInterface\n    {\n        $formatterType = $config['formatter'] ?? 'json';\n        \n        if ($formatterType === 'line') {\n            return new LineFormatter(\n                $config['format'] ?? null,\n                $config['date_format'] ?? null\n            );\n        }\n        \n        return new JsonFormatter();\n    }\n    \n    /**\n     * {@inheritdoc}\n     */\n    protected function write(string $level, string $message, array $context = []): void\n    {\n        // Format log data\n        $logData = [\n            'timestamp' =\u003e time(),\n            'level' =\u003e $level,\n            'message' =\u003e $message,\n            'context' =\u003e $context\n        ];\n        \n        // Publish to Redis channel\n        $this-\u003eredis-\u003epublish(\n            $this-\u003echannel,\n            json_encode($logData)\n        );\n    }\n}\n```\n\n### The `create()` Method\n\nThe static `create()` method is responsible for instantiating your logger based on configuration:\n\n```php\npublic static function create(array $config): LoggerInterface\n{\n    // Create dependencies based on configuration\n    // ...\n    \n    // Return new logger instance\n    return new self(...);\n}\n```\n\nThis method receives the channel configuration from the `logging.php` config file and should:\n\n1. Create any dependencies the logger needs\n2. Configure those dependencies based on the config array\n3. Return a new instance of the logger\n\n### The `write()` Method\n\nThe `write()` method is where the actual logging happens:\n\n```php\nprotected function write(string $level, string $message, array $context = []): void\n{\n    // Implement logging logic here\n}\n```\n\nThis method is called by the parent `AbstractLogger` class when a log message needs to be written. It receives:\n\n- `$level`: The log level (debug, info, warning, etc.)\n- `$message`: The formatted log message\n- `$context`: Additional context data\n\n## Using Custom Loggers\n\n### Method 1: Configuration-Based Discovery\n\nThe simplest way to use a custom logger is to specify the fully-qualified class name in your logging configuration:\n\n```php\n// In config/logging.php\n'channels' =\u003e [\n    'redis' =\u003e [\n        'driver' =\u003e 'redis',\n        'class' =\u003e \\App\\Logging\\RedisLogger::class,\n        'host' =\u003e env('REDIS_HOST', '127.0.0.1'),\n        'port' =\u003e env('REDIS_PORT', 6379),\n        'channel' =\u003e 'application_logs',\n        'level' =\u003e 'debug',\n    ],\n]\n```\n\nWhen you specify a `class` parameter, that class will be used regardless of the driver name.\n\n### Method 2: Driver Name Registration\n\nYou can register your logger with a driver name, which allows you to reference it using just the driver name:\n\n```php\n// In a service provider's register method\n$this-\u003eapp-\u003emake(\\Ody\\Logger\\LogManager::class)\n    -\u003eregisterDriver('redis', \\App\\Logging\\RedisLogger::class);\n```\n\nThen in your configuration:\n\n```php\n// In config/logging.php\n'channels' =\u003e [\n    'redis' =\u003e [\n        'driver' =\u003e 'redis', // This will use the registered RedisLogger\n        'host' =\u003e env('REDIS_HOST', '127.0.0.1'),\n        'port' =\u003e env('REDIS_PORT', 6379),\n        'channel' =\u003e 'application_logs',\n        'level' =\u003e 'debug',\n    ],\n]\n```\n\n### Method 3: Automatic Discovery\n\nIf your logger follows the naming convention `{Driver}Logger` and is in one of the registered namespaces, it will be discovered automatically:\n\n```php\n// In config/logging.php\n'channels' =\u003e [\n    'redis' =\u003e [\n        'driver' =\u003e 'redis', // Will look for RedisLogger\n        // Configuration...\n    ],\n]\n```\n\nThe framework will search for `RedisLogger` in the registered namespaces (`\\Ody\\Logger\\` and `\\App\\Logging\\` by default).\n\n### Creating Custom Formatters\n\nIf the standard formatters don't meet your needs, you can create your own by implementing the `FormatterInterface`:\n\n```php\nnamespace App\\Logging;\n\nuse Ody\\Logger\\FormatterInterface;\n\nclass CustomFormatter implements FormatterInterface\n{\n    public function format(string $level, string $message, array $context = []): string\n    {\n        // Custom formatting logic\n        return \"[$level] $message \" . json_encode($context);\n    }\n}\n```\n\n## Complete Example: Using Redis Logger\n\n### Configuration\n\n```php\n// In config/logging.php\n'channels' =\u003e [\n    // Using explicit class\n    'redis' =\u003e [\n        'driver' =\u003e 'redis',\n        'class' =\u003e \\App\\Logging\\RedisLogger::class,\n        'host' =\u003e env('REDIS_HOST', '127.0.0.1'),\n        'port' =\u003e env('REDIS_PORT', 6379),\n        'password' =\u003e env('REDIS_PASSWORD', null),\n        'channel' =\u003e 'app_logs',\n        'formatter' =\u003e 'json',\n        'level' =\u003e 'debug',\n    ],\n    \n    // Using it in a stack\n    'production' =\u003e [\n        'driver' =\u003e 'group',\n        'channels' =\u003e ['file', 'redis'],\n    ],\n],\n```\n\n### Usage\n\n```php\n// Send to redis channel\nlogger('User registered', ['id' =\u003e 123], 'redis');\n\n// Or use the stack\nlogger('API request processed', ['endpoint' =\u003e '/users']);\n```\n---\n\nWith this system, you can create custom loggers that integrate seamlessly with the ODY Framework logging infrastructure.\n\n## Service Providers\n\nService providers are used to register services with the application. Custom service providers can be created in the \n`app/Providers` directory:\n\n```php\n\u003c?php\n\nnamespace App\\Providers;\n\nuse Ody\\Foundation\\Providers\\ServiceProvider;\n\nclass CustomServiceProvider extends ServiceProvider\n{\n    public function register(): void\n    {\n        // Register bindings\n        $this-\u003esingleton('custom.service', function() {\n            return new CustomService();\n        });\n    }\n    \n    public function boot(): void\n    {\n        // Bootstrap services\n    }\n}\n```\n\nRegister your service provider in `config/app.php`:\n\n```php\n'providers' =\u003e [\n    // Framework providers\n    Ody\\Foundation\\Providers\\DatabaseServiceProvider::class,\n    \n    // Application providers\n    App\\Providers\\CustomServiceProvider::class,\n],\n```\n\n## Running the Application\n\n### Development Server\n\nFor development, you can use the built-in PHP server:\n\n```bash\nphp -S localhost:8000 -t public\n```\n\n### Production Deployment\n\nFor production, you can use Swoole HTTP server:\n\n```bash\nphp ody serve --swoole --host=0.0.0.0 --port=9501\n```\n\n## Advanced Features\n\n### Custom Commands\n\nCreate custom console commands by extending the base Command class:\n\n```php\n\u003c?php\n\nnamespace App\\Console\\Commands;\n\nuse Ody\\Foundation\\Console\\Command;\n\nclass CustomCommand extends Command\n{\n    protected $name = 'app:custom';\n    \n    protected $description = 'A custom command';\n    \n    protected function handle(): int\n    {\n        $this-\u003einfo('Custom command executed successfully!');\n        \n        return self::SUCCESS;\n    }\n}\n```\n\n### Working with Databases\n\nThe framework provides a simple database abstraction layer:\n\n```php\n\u003c?php\n\n// Using PDO directly\n$db = app('db');\n$users = $db-\u003equery('SELECT * FROM users')-\u003efetchAll();\n\n// Or inject PDO into your classes\npublic function __construct(PDO $db)\n{\n    $this-\u003edb = $db;\n}\n```\n\n## Resources\n\n- [GitHub Repository](https://github.com/ody-dev/ody-core)\n- [Issue Tracker](https://github.com/ody-dev/ody-core/issues)\n- [PSR Standards](https://www.php-fig.org/psr/)\n- [Swoole Documentation](https://www.swoole.co.uk/docs/)\n\n## License\n\nODY Framework is open-source software licensed under the MIT license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fody-dev%2Fframework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fody-dev%2Fframework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fody-dev%2Fframework/lists"}