{"id":37005610,"url":"https://github.com/reves/val","last_synced_at":"2026-01-14T00:40:44.974Z","repository":{"id":174973863,"uuid":"337138713","full_name":"reves/val","owner":"reves","description":"val is a PHP web framework mainly for single page applications","archived":false,"fork":false,"pushed_at":"2025-04-17T18:00:27.000Z","size":261,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-23T07:06:45.929Z","etag":null,"topics":["backend-framework","php"],"latest_commit_sha":null,"homepage":"https://packagist.org/packages/reves/val","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/reves.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":"2021-02-08T16:33:07.000Z","updated_at":"2025-04-22T16:25:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"ac3c629f-1b9e-4b02-8e4b-d4d75af1834c","html_url":"https://github.com/reves/val","commit_stats":null,"previous_names":["reves/val"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/reves/val","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reves%2Fval","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reves%2Fval/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reves%2Fval/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reves%2Fval/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reves","download_url":"https://codeload.github.com/reves/val/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reves%2Fval/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28406520,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T21:51:37.118Z","status":"ssl_error","status_checked_at":"2026-01-13T21:45:14.585Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["backend-framework","php"],"created_at":"2026-01-14T00:40:44.857Z","updated_at":"2026-01-14T00:40:44.953Z","avatar_url":"https://github.com/reves.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eval\u003c/h1\u003e\n\nval is a PHP web framework mainly for single page applications.\n\n#### Requirements\n\n- PHP \u003e= 8.3\n- Composer\n- Mbstring PHP Extension\n- Sodium PHP Extension\n\n#### Out of the box compatibility\n\n- Databases: SQLite, PostgreSQL, MySQL / MariaDB\n- Servers: Apache, Nginx\n\n#### Table of contents:\n\n- [Installation](#installation)\n- [How it works](#how-it-works)\n- [Conventions](#conventions)\n- [CLI](#cli)\n- [Configuring the server](#configuring-the-server)\n- [Configuring the app and environments](#configuring-the-app-and-environments)\n- [Serving the View](#serving-the-view)\n- [Creating an API](#creating-an-api)\n- [Migrations](#migrations)\n- [Documentation](#documentation)\n\n## Installation\n\n#### 1. Add to `composer.json` the following script that will automatically create the CLI tool on installation.\n```json\n\"scripts\": {\n    \"post-update-cmd\": \"Val\\\\Console::create\"\n},\n```\n#### 2. Run the installation.\n```console\ncomposer require reves/val\n```\n\n## How it works\n\nUsage example in `public/index.php`:\n\n```php\n\u003c?php\n\nrequire __DIR__.'/../vendor/autoload.php';\n\nuse Val\\App;\n\nApp::run(function() {\n\n    echo 'Hello, World!';\n\n});\n```\n\nThe entry point of the application is the static method `Val\\App::run()`, which is called in `index.php`. This method processes two types of requests:\n\n#### 1. API Requests\nAPI requests are either `POST` or `GET`, and they require the `_api` parameter to be present in the request URL (e.g. `/index.php?_api=books`). When this parameter is detected, the framework will use the corresponding API class located in your application's `api/` directory (e.g. `api/Books.php`).\n\nIn this case the application acts as an API provider, responding with raw JSON data and appropriate HTTP status codes. For API client convenience on frontend, the server should have rewrite rules configured to handle clean URLs, such as `/api/books`.\n\n#### 2. View Requests\nFor all non-API requests, the application serves the **View**, which is simply an anonymous function passed as the first parameter to ``App::run()``. For example, this function may return HTML of a single-page application, where routing is handled on the client-side.\n\n## Conventions\n\n### Root directory structure\n- `api/` – Contains API classes.\n- `config/` – Application and environment configuration files.\n- `migrations/` – Database migration classes.\n- `public/` – The directory exposed to the internet, containing the `index.php` entry point.\n- `vendor/` – Dependencies managed by Composer.\n- `view/` – Contains templates for the **View** function or email templates.\n- ``.gitignore`` – **(!) Important:** Ensure files like `config/env.*` are added.\n- ``val`` *(or ``val.bat`` on windows)* – The CLI tool used for app files creation and migration management.\n\n### Timezone\n\nTimezone defaults to UTC.\n\n## CLI\n\nThe `val` file, located in the root directory of the app, represents the CLI tool. It helps with files creation and database migration management.\n\n```console\nval command [subcommand] [\u003cargument\u003e]\n```\n\n\n### Commands:\n\n#### `val` or `val help`\n  Lists all the available commands.\n\n#### `val create`\n\nCreates all the necessary directories and files for the app to function. Useful command especially for new projects.\n\n#### Subcommands of `val create`:\n\n- `val create config \u003cname\u003e` – Creates a specific config file. E.g. `val create config geolocation` will create the `config/geolocation.php` file. If the `\u003cname\u003e` is not specified, will create a config file named `config/config.php`.\n  \n  The `app`, `auth`, `db`, `env` or `envdev` config names are reserved and will use a built-in template for creation.\n\n- `val create api \u003cname\u003e` – Creates a specific API class. E.g. `val create api Books` will create the `api/Books.php` file. If the `\u003cname\u003e` is not specified, will create an API class file named `api/Test.php`.\n\n- `val create migration \u003cname\u003e` – Creates a new migration class. E.g. `val create migration CreateBooksTable` will create the `migrations/id_CreateBooksTable.php`. If the `\u003cname\u003e` is not specified, will create a migration class file named `migrations/id_NewMigration.php`.\n\n  The migration name `CreateSessionsTable` is reserved and will use a built-in template for creation.\n\n- `val create appkey` – Creates the `config/env.dev.php` env config file with a generated app key or, if the file already exists, regenerates the current app key in that file. This command will not change the app key in the `config/env.php` (production env config).\n\n#### `val migrate \u003cversion\u003e [-y/--yes]`\n\nRuns all migrations till (including) the specified version number. E.g. `val migrate 5` will call consecutively all `up()` methods of the new migrations where `id \u003c= 5`. If the `\u003cversion\u003e` is not specified, will attempt to migrate to the latest available version.\n\n(!) This command asks for confirmation [y/n] before proceeding.\n\n#### `val rollback \u003cversion\u003e [-y/--yes]`\n\nRollbacks all migrations until (excluding) the specified version number. E.g. `val rollback 3` will call consecutively all `down()` methods of the applied migrations where `id \u003e 3`. If the `\u003cversion\u003e` is not specified, will attempt to rollback the latest applied version.\n\n(!) This command asks for confirmation [y/n] before proceeding.\n\n## Configuring the server\n\n### Apache\n\nThe `val create` command will create an `.htaccess` file in `/public` directory.\n\n### Nginx\n\n[TODO]\n\n## Configuring the app and environments\n\n### Environment\n\nThe environment configuration files are the two config files with reserved names `config/env.php` and `config/env.dev.php`. At least one of these two files is required, to run the application.\n\n```console\nval create config env\nval create config envdev\n```\n\n- `env.php` – Production environment configuration file.\n- `env.dev.php` – Development environment configuration file. If this file is present, it takes precedence over `env.php`, so the environment becomes of development. If this file is missing, the default environment is production.\n\n#### Environment-dependant side effects:\n- The `display_errors` ini setting is set to `false` in production environment.\n\n### App configurations\n\nThe main app configuration files are the config files located in the `config/` folder.\n\nReserved config file names:\n\n```console\nval create config app\nval create config auth\nval create config db\n```\n\n## Serving the View\n\n[TODO] (meanwhile, read [App class](#app-class-valapp))\n\n## Creating an API\n\n[TODO] (meanwhile, read [Api class](#api-class-valapi))\n\n## Migrations\n\n[TODO] (meanwhile, read [CLI](#cli))\n\n# Documentation\n\n- [App class](#app-class-valapp)\n- [Api class](#api-class-valapi)\n- [App-related modules](#app-related-modules)\n- [Api-related modules](#api-related-modules)\n\n### App class `Val\\App`\n\nThe `App` class manages the application runtime.\n\n#### Usage\n\n`App::run(?\\Closure $view = null, ?string $rootPath = null) : void`\n\nThe method `run()` represets the application entry point and initializes all modules, directory paths and specific response headers, then runs the API/View (depending on request type).\n\n```php\n// in index.php\nApp::run(function() {}, \"/custom/root/dir\");\n```\n```php\n// Application entry point, both for View and API.\n// Note: the anonymous function represents the View, has nothing to do with APIs.\nApp::run(function() {\n    echo 'Hello, World!';\n});\n```\n\n`App::isApiRequest() : bool`\n\n```php\nApp::isApiRequest(); // === isset($_GET['_api'])\n```\n\n`App::isProd() : bool`\n\n```php\nApp::isProd();\n// true:  only \"config/env.php\" exists\n// false: only \"config/env.dev.php\" exists\n// false: both \"config/env.php\" and\"config/env.dev.php\" exist\n```\n\n`App::exit() : never`\n\n```php\nApp::exit(); // closes the DB connection (if any) and terminates the script execution.\n```\n\n### Api class `Val\\Api`\n\nThe `Api` class offers request/response management for application's API classes. In other words, classes in the `api/` directory should extend the `Api` class (unless a specific API class manages the request/response process in a custom way).\n\n#### Functionality\n- Setting request requirements for a specific method (endpoint):\n  - the allowed request method: only POST / only GET / both (default).\n  - authentication: only Authenticated users / only Unauthenticated users / both (default).\n  - required field(s).\n  - optional field(s).\n- Validation/Pre-processing of field values – automatic calls to corresponding methods (if defined, e.g. `ApiName::validate\u003cFieldname\u003e(\u0026$value)`), based on specified required/optinal field(s).\n- Response structuring:\n  - preparing a common error response, by setting `setInvalid()` for each field with invalid data.\n  - automatically sent `400` response, if any missing or invalid fields, after all validation methods calls. The error-related data is included in the response.\n  - successful response `200`, with/without response data.\n  - custom error `400...500` response.\n\n#### Usage\n\n`Api::__invoke()`\n\nThis method represents the default endpoint of a specific API (e.g. `example.com/api/books`).\n\nBy default, this method is empty and returns a `404` response.\n\n```php\n// e.g. in api/Books.php\n\nuse Val\\Api;\n\nFinal Class Books Extends Api\n{\n    public function __invoke()\n    {\n        $this-\u003eonlyGET();\n        // e.g. getting the list of books from database ...\n        $list = [\n            ['title' =\u003e 'Bright Days', 'author' =\u003e 'John Doe', 'year' =\u003e 2005],\n            ['title' =\u003e 'The Shadows', 'author' =\u003e 'Michael Smith', 'year' =\u003e 2006],\n            ['title' =\u003e 'The Last Ember', 'author' =\u003e 'Jonathan Blake', 'year' =\u003e 2008],\n        ];\n        // Sends a response with status \"200\" and JSON-encoded $list data.\n        // The \"return\" is optional, but recommended, especially when using \n        // Api::peek() internal calls.\n        return $this-\u003esuccess($list);\n    }\n}\n```\n\nGET `/api/books` - `200 OK`\n\n```json\n[\n    {\"title\":\"Bright Days\",\"author\":\"John Doe\",\"year\":2005},\n    {\"title\":\"The Shadows\",\"author\":\"Michael Smith\",\"year\":2006},\n    {\"title\":\"The Last Ember\",\"author\":\"Jonathan Blake\",\"year\":2008}\n]\n```\n\n`Api::peek(string $endpoint, array $params = []) : mixed`\n\nThis method makes an internal call to any application API in a \"frontend-like\" format.\n\n```php\n// e.g. in api/Books.php\nFinal Class Books Extends Api\n{\n    public function __invoke() {/*...*/}\n\n    public function byAuthor()\n    {\n        $this-\u003eonlyGET()-\u003erequired('author');\n\n        $list = Api::peek('/books'); // calls Books::__invoke(), without parameters\n        $author = $this-\u003eval('author');\n        $result = array_filter($list, function($v) use ($author) {\n            return $v['author'] === $author;\n        });\n\n        return $this-\u003esuccess($result);\n    }\n}\n```\nGET `/api/books/byauthor?author=John+Doe` - `200 OK`\n\n(notice the `author` parameter, which is required)\n\n```json\n[\n    {\"title\":\"Bright Days\",\"author\":\"John Doe\",\"year\":2005}\n]\n```\nCan be used from the View function as well:\n```php\n// e.g. in public/index.php\nVal\\App::run(function() {\n    echo '\u003cpre\u003e';\n    print_r(Val\\Api::peek('/books')); // prints the returned $list from __invoke()\n    echo '\u003c/pre\u003e';\n});\n```\n\n`Api::onlyGET() : self`\n\nAllows only GET requests to **this** API action method.\n\n```php\npublic function byAuthor() {\n    $this-\u003eonlyGET();\n    // ...\n}\n```\n\n`Api::onlyPOST() : self`\n\nAllows only GET requests to **this** API action method.\n\n```php\npublic function subscribe() {\n    $this-\u003eonlyPOST();\n    // ...\n}\n```\n\n`Api::onlyAuthenticated() : self`\n\nAllows only Authenticated users to call **this** API action method.\n\n```php\npublic function update() {\n    $this-\u003eonlyPOST()-\u003eonlyAuthenticated();\n    // ...\n}\n```\n\n`Api::onlyUnauthenticated() : self`\n\nAllows only Unauthenticated users to call **this** API action method.\n\n```php\npublic function claimFirstTimeDiscount() {\n    $this-\u003eonlyPOST()-\u003eonlyUnauthenticated();\n    // ...\n}\n```\n\n`Api::required(string|array ...$fields) : self`\n\nRegisters the required (mandatory) fields for **this** API action method. Also, calls the corresponding validation method (if defined) for each field.\n\n**(!) Fields with empty `\"\"` values are NOT considered as missing, so remember to check for length (when validating), if needed.**\n\n```php\npublic function list() {\n    $this-\u003eonlyGET()-\u003erequired('author', 'year', 'title');\n    // ...\n}\n```\n\nPOST `/api/\u003capi-name\u003e/list?author=John+Doe\u0026title=` - `400 Bad Request`\n\n```json\n{\n    \"missing\": [\"year\"]\n}\n```\n\nValidation:\n\n```php\npublic function addComment() {\n    $this-\u003eonlyPOST()\n        -\u003eonlyAuthenticated()\n        -\u003erequired('name', 'age', 'email', 'message');\n\n        $name = $this-\u003eval('name'); // after validation (trimmed value)\n        $age =  $this-\u003eval('age'); // integer\n        // ...\n}\n\n// e.g. validation method, which is called automatically (if defined):\n// Convention: protected function validate\u003cFieldname\u003e($value)\nprotected function validateName(\u0026$name) // `\u0026` means that field value will be modified\n{\n    $name = trim($name);\n    if (!$name) return 'EMPTY_VALUE';\n    if (mb_strlen($name) \u003e 100) {\n        \n        return ['TOO_LONG', ['max' =\u003e 100]];\n\n        // ... or manually:\n        // $this-\u003esetInvalid('name', 'TOO_LONG', ['max' =\u003e 100]);\n    }\n}\n\n// e.g. type conversion inside validation\nprotected function validateAge(\u0026$age)\n{\n    $age = intval(trim($age));\n    // ...\n}\n```\n\nPOST `/api/\u003capi-name\u003e/addComment` - `400 Bad Request`\n\n```json\n{\n    \"invalid\": {\n        \"name\": {\n            \"status\": \"TOO_LONG\",\n            \"params\": {\"max\": 100}\n        }\n    }\n}\n```\n\nGrouping fields in arrays, to use the same validation method:\n\n```php\npublic function register() {\n    $this-\u003eonlyPOST()\n        -\u003eonlyUnauthenticated();\n        -\u003erequired(\n            'email',\n            ['firstName', 'lastName'],\n            'password'\n        );\n    // ...\n}\n\n// e.g. validation method for the grouped fields 'firstName' and 'lastName':\n// Convention: \u003cFieldname\u003e is the name of the first field in the grouping array\nprotected function validateFirstName($name) {/*...*/} // same validator for both fields \n```\n\n`Api::optional(string|array ...$fields) : self`\n\nWorks the same way as `Api::required` does, except it doesn't list these fields as `missing` if they're not specified in the request. Also calls the corresponding validation method for each field (if defined).\n\n```php\n$this-\u003eoptional('note', ['address1', 'address2']);\n```\n\n`Api::val(string $field) : mixed`\n\nReturns the value of the specified field.\n\n```php\npublic function register() {\n    $this-\u003eonlyPOST()-\u003eonlyUnauthenticated()-\u003erequired('email', 'password');\n    $email = $this-\u003eval('email'); // getting the value after validation (if defined):\n    // ...\n}\n\nprotected function validatePassword($pw)\n{\n    // e.g. getting the value of another field inside a validation method:\n    $email = $this-\u003eval('email');\n    if ($pw === $email) return 'PASSWORD_MATCHES_EMAIL';\n}\n```\n\n`Api::setInvalid(string $field, string $status, ?array $params = null) : self`\n\nManually adds to the final error response an \"invalid\" message for a specific field.\n\n```php\npublic function register() {\n    $this-\u003eonlyPOST()-\u003eonlyUnauthenticated()-\u003erequired('email', 'password');\n\n    // Manual validation:\n    if (!mb_strlen($this-\u003eval('email'))) {\n        $this-\u003esetInvalid('email', 'EMPTY_VALUE');\n    }\n    //...\n}\n```\n\n`Api::success(?array $data = null) : array|bool`\n\n```php\npublic function list()\n{\n    //...\n    return $this-\u003esuccess($list); // status 200 and JSON-encoded $list in the response body\n}\n```\n\n```php\npublic function addImage()\n{\n    //...\n    return $this-\u003esuccess(); // status 200\n}\n```\n\n`Api::error(int $code = 500, ?string $status = null) : never`\n\nResponds with an error. This methods is also used internally in the process of fields validation.\n\nIf the requirements like `onlyPOST` or `onlyAuthenticated` are met (if any), then the following error messages will appear (if any) in the response body, in JSON format, in the following order, one at a time:\n\n`missing` `400` --\u003e `invalid` `400` --\u003e `status` `\u003cstatus-code\u003e` (custom error)\n\n```php\npublic function addImage()\n{\n    //...\n    if (!$savedOnDisk) return $this-\u003eerror(); // status 500 by default\n    //...\n}\n```\n\nWith a custom status/message:\n\n```php\npublic function addImage()\n{\n    //...\n    if (!$savedInDatabase) return $this-\u003eerror(500, 'CUSTOM_STATUS or a verbose message.');\n    //...\n}\n```\n\nPOST `/api/\u003capi-name\u003e/addImage` - `500 Internal Server Error`\n\n```json\n{\n    \"status\": \"CUSTOM_STATUS or a verbose message.\"\n}\n```\n\n## App-related modules\nThe App-related modules are used internally in the framework, also may be used in the application's APIs or in the View as well.\n\nThe modules initialized by default are: `Lang`, `CSRF`, `DB`, `Auth`, `Renderer`. Although the modules `Lang`, `DB` and `Auth` require certain configs to work.\n\n- [Auth](#auth-valappauth)\n- [Config](#config-valappconfig)\n- [Cookie](#cookie-valappcookie)\n- [Crypt](#crypt-valappcrypt)\n- [CSRF](#csrf-valappcsrf-internal)\n- [DB](#db-valappdb-and-valappdbdriver)\n- [HTTP](#http-valapphttp)\n- [JSON](#json-valappjson)\n- [Lang](#lang-valapplang)\n- [Renderer](#renderer-valapprenderer)\n- [Token](#token-valapptoken)\n- [UUID](#uuid-valappuuid)\n\n### Auth `Val\\App\\Auth`\n\nThe Auth module allows to:\n- **Authenticate the user account** - checks whether the session token is genuine and not expired/revoked.\n- **Manage account's sessions** (on multiple devices).\n\n**(!) The account creation/confirmation/management, access authorization, roles and other... will be managed by application's custom APIs which will use the framework's Auth module just as a sessions management library.**\n\n**(!) The `accountId` is expected to be of `UUIDv7` format (use the `Val\\App\\UUID` module for generation).**\n\n#### Configs `config/auth.php` (required)\n\n- (optional) `session_lifetime_days =\u003e 365` – The session will permanently expire after this duration (in days).\n  - Default: `Auth::SESSION_LIFETIME_DAYS`\n- (optional) `session_max_offline_days =\u003e 7` – The session will expire if the device remains inactive for this duration (in days).\n  - Default: `Auth::SESSION_MAX_OFFLINE_DAYS`\n- (optional) `token_trust_seconds =\u003e 5` – Duration (in seconds) to trust the session token before re-checking in the database if the session still remains valid.\n  - Default: `Auth::TOKEN_TRUST_SECONDS`\n- (optional) `session_update_seconds =\u003e 60` – The \"last seen\" data is updated in the database no more frequently than this duration (in seconds).\n  - Default: `Auth::SESSION_UPDATE_SECONDS`\n- (optional) `max_active_sessions =\u003e 30` – The maximum number of active sessions per account.\n  - Default: `Auth::MAX_ACTIVE_SESSIONS`\n\n- Other configs:\n  - `config/db.php` (required), and the default `CreateSessionsTable` migration applied (use the CLI).\n  - `config/app.php` (required).\n\n#### Usage\n\n`Auth::initSession(string $accountId) : bool`\n\nInitializes a new session for a given accountId (UUID). Returns true on success, or false on error or if too many active sessions.\n\n```php\n// e.g. in api/Account.php\npublic function signIn()\n{\n    $this-\u003eonlyPOST()\n        -\u003eonlyUnauthenticated()\n        -\u003erequired('username', 'password');\n\n    // getting the user from the database ...\n    // checking password ...\n    // maybe 2-Factor-Authentication ...\n\n    return Auth::initSession($accountId); // creates a new session in database and sets the session cookie\n        ? $this-\u003erespondSuccess()\n        : $this-\u003erespondError();\n}\n```\n\n`Auth::revokeSession(?string $id = null) : bool`\n\nRevokes the authentication session by a given session UUID. If no parameter given, revokes the current session. Returns true on success, or false if the session is not found in the database.\n\n```php\n// e.g. in api/Account.php\npublic function signOut()\n{\n    $this-\u003eonlyPOST()\n        -\u003eonlyAuthenticated();\n\n    return Auth::revokeSession() // deletes the session from database and removes the cookie\n        ? $this-\u003erespondSuccess()\n        : $this-\u003erespondError();\n}\n```\n\n`Auth::revokeAllSessions() : bool`\n\nRevokes all the sessions of the current user. Returns true on success, or false if no sessions were found in the database.\n\n```php\n// e.g. in api/Account.php\npublic function signOutFromAllDevices()\n{\n    $this-\u003eonlyPOST()\n        -\u003eonlyAuthenticated();\n\n    return Auth::revokeAllSessions()\n        ? $this-\u003erespondSuccess()\n        : $this-\u003erespondError();\n}\n```\n\n`Auth::revokeOtherSessions() : bool`\n\nRevokes all the sessions of the current user, **except the current session**. Returns true on succes, or false if no other sessions were found in the database.\n\n```php\n// e.g. in api/Account.php\npublic function signOutFromOtherDevices()\n{\n    $this-\u003eonlyPOST()\n        -\u003eonlyAuthenticated();\n\n    return Auth::revokeOtherSessions()\n        ? $this-\u003erespondSuccess()\n        : $this-\u003erespondError();\n}\n```\n\n`Auth::removeExpiredSessions(string $accountId) : bool`\n\nRemoves all the expired sessions of a given account UUID from the database. Returns true on success, or false if no expired sessions were found in the database.\n\n**(!) This method is automatically called on every Auth::initSession() call.**\n\n```php\n// in a cron job (e.g. for cases when the user never signed in again)\nAuth::removeExpiredSessions($accountId);\n```\n\n`Auth::getAccountId() : ?string`\n\nReturns the accountId (UUID) associated with the current session, or null if the user is unauthenticated.\n\n```php\n// e.g. in api/Account.php\npublic function isAuthenticated()\n{\n    $this-\u003eonlyGET();\n\n    return $this-\u003erespondData([\n        'isAuthenticated' =\u003e (Auth::getAccountId() !== null)\n    ]);\n}\n```\n\n`Auth::getSignedInAt() : ?string`\n\nReturns the dateTime the session was initialized.\n\n```php\nif (Auth::getAccountId() !== null) {\n    $dateTime = Auth::getSignedInAt() // e.g. '2025-01-01 00:00:00'\n}\n```\n\n`Auth::getLastSeenAt() : ?string`\n\nReturns the dateTime the session data updated.\n\n`Auth::getSignedInIPAddress() : ?string`\n\nReturns the IP address of the session initialization.\n\n`Auth::getLastSeenIPAddress() : ?string`\n\nReturns the IP address of the session update.\n\n`Auth::getIPAddress() : ?string`\n\nA helper static method that returns the client's IP address, or null if unable to determine.\n\n### Config `Val\\App\\Config`\n\nThe Config module allows to get a config/env value by specifying the config name and the field name.\n\n#### Usage\n\n`Config::\u003cconfig_name\u003e(string $name) : mixed`\n\nThe dynamic `\u003cconfig_name\u003e` method stands for the config file name and the `$name` parameter stands for the field name inside the config file.\n\n```php\n// In config/app.php\n'foo' =\u003e 'bar',\n\n// ... then\n$value = Config::app('foo'); // 'bar'\n```\n```php\n// Get env variable (automatically chooses from which one - env.php or env.dev.php)\n$value = Config::env('test');\n```\n```php\n// In config/custom.php\nreturn [\n    'something' =\u003e Config::env('test'), // get value from the current env\n    'foofoo' =\u003e Config::app('foo') . 'bar', // get value from another config\n];\n\n// ... then\n$value = Config::custom('foofoo'); // 'barbar'\n```\n\n### Cookie `Val\\App\\Cookie`\n\n#### Usage\n\n`Cookie::isSet(string $name) : bool`\n\n```php\n$isSet = Cookie::isSet('cookiename');\n```\n\n`Cookie::get(string $name) : string`\n\n```php\n$value = Cookie::get('cookiename'); // may return empty string if the cookie is not set\n```\n\n`Cookie::set(string $name, string $value = '', array $options = []) : bool`\n\n```php\n$options = [ // these are also the default options values:\n    'expires'   =\u003e 0,\n    'path'      =\u003e '/',\n    'domain'    =\u003e '',\n    'secure'    =\u003e true,\n    'httponly'  =\u003e true,\n    'samesite'  =\u003e 'Lax'\n];\n\n$result1 = Cookie::set('name1', 'value1'); // use default options\n$result2 = Cookie::set('name2', 'value2', $options);\n$result3 = Cookie::set('name3', 'value3', ['httponly' =\u003e false]); // change a specific option\n```\n\n`Cookie::unset(string $name) : bool`\n\n```php\n$result = Cookie::unset('cookiename');\n```\n\n`Cookie::setForDays(string $name, string $value = '', int $days = 1, array $options = []) : bool`\n\n```php\n$result = Cookie::setForDays('cookiename', 'value'); // 1 day\n$result = Cookie::setForDays('cookiename', 'value', 7, ['httponly' =\u003e false]); // 7 days, with a custom option\n```\n\n`Cookie::setForMinutes(string $name, string $value = '', int $minutes = 1, array $options = []) : bool`\n\n`Cookie::setForSeconds(string $name, string $value = '', int $seconds = 1, array $options = []) : bool`\n\n\n### Crypt `Val\\App\\Crypt`\n\n#### Configs `config/app.php` (required)\n\n- (required) `key =\u003e 'app-key'` – required for this module to work.\n\n#### Usage\n\n`Crypt::encrypt(?string $message) : ?string`\n\n```php\n$encrypted = Crypt::encrypt('an important message'); // may return null\n```\n\n`Crypt::decrypt(string $encodedEncryptedMessage) : ?string`\n\n```php\n$decrypted = Crypt::decrypt($encrypted); // may return null\n```\n\n### CSRF `Val\\App\\CSRF` (internal)\n\nThe CSRF module is automatically applied for the API methods which have `$this-\u003eonlyPOST()` set.\n\n#### Configs `config/app.php` (required)\n\n- (required) `key =\u003e 'app-key'` – required for encryption.\n\n### DB `Val\\App\\DB` and `Val\\App\\DBDriver`\n\nThe DB module wraps the `PDO` interface and makes it easier to use.\n\n#### Configs `config/db.php` (required)\n\n- (optional, but recommended) `driver =\u003e DBDriver::MySQL` – the database driver.\n  - `DBDriver::MySQL` (default) – MySQL, compatible with MariaDB.\n  - `DBDriver::PostgreSQL` – PostgreSQL.\n  - `DBDriver::SQLite` – SQLite.\n- For SQLite driver:\n  - (required) `path =\u003e App::$DIR_ROOT . '/db.sqlite3'` – Path to database.\n- For MySQL or PostgreSQL driver:\n  - (required) `'host' =\u003e '127.0.0.1'` – Database host.\n  - (required) `'db' =\u003e 'myapp'` – Database name.\n  - (required) `'user' =\u003e 'root'` – Database user.\n  - (required) `'pass' =\u003e ''` – Database user password.\n\n#### Usage\n\n`DB::beginTransaction() : bool`\n\n`DB::commit() : bool`\n\n```php\nDB::beginTransaction();\n// ...\nDB::commit();\n```\n\n`DB::rollback() : bool`\n\n`DB::transactionIsActive() : bool`\n\n```php\nDB::beginTransaction();\n// ...\nif ($somethingHappened) {\n    DB::rollback(); // cancels the current transaction\n}\nDB::transactionIsActive(); // `false`\n// ...\nDB::commit(); // will commit only if the transacrion is still active, safe to use here\n```\n\n`DB::raw(string $query) : int|bool`\n\n**(!) Data inside the query should be properly escaped.**\n\nExecutes an SQL statement with a custom query. This method cannot be used with any queries that return results. Returns the number of rows that were modified or deleted, or false on error.\n\n```php\n$count = DB::raw(\"DELETE FROM books\");\n```\n\n`DB::lastInsertId() : string`\n\n**(!) In case of a transaction, should be used before `DB::commit()`.**\n\nReturns the id `string` of the last inserted row.\n\n```php\n$id = DB::lastInsertId();\n```\n\n`DB::rowCount() : int`\n\n**(!) Not recommended to use with SELECT statements.**\n\nReturns the number of rows affected by the last DELETE, INSERT, or UPDATE statement. \n\n`DB::prepare(string $query) : self`\n\n```php\n$db = DB::prepare(\"SELECT title FROM books\"); // returns the DB instance, for convenience\n```\n\n`DB::bind(string|int $placeholder, bool|float|int|string|null $value) : self`\n\n```php\n$db = DB::prepare(\"SELECT title FROM books WHERE author = :author AND published = :published\")\n    -\u003ebind(':author', 'John Doe')\n    -\u003ebind(':published', true);\n```\n```php\n$db = DB::prepare(\"SELECT title FROM books WHERE author = ? AND published = ?\")\n    -\u003ebind(1, 'John Doe')\n    -\u003ebind(2, true);\n```\n\n`DB::bindPlaceholder(bool|float|int|string|null $value) : self`\n\n```php\n$db = DB::prepare(\"SELECT title FROM books WHERE author = ? AND published = ?\")\n    -\u003ebindPlaceholder('John Doe') // autoindex to 1\n    -\u003ebindPlaceholder(true); // autoindex to 2\n```\n\n`DB::bindMultiple(array $relations) : self`\n\n```php\n$db = DB::prepare(\"SELECT title FROM books WHERE author = :author AND published = :published\")\n    -\u003ebindMultiple([\n        ':author' =\u003e 'John Doe',\n        ':published' =\u003e true,\n    ]); // uses DB::bind() for each entry\n```\n```php\n$db = DB::prepare(\"SELECT title FROM books WHERE author = ? AND published = ?\")\n    -\u003ebindMultiple([\n        1 =\u003e 'John Doe',\n        2 =\u003e true,\n    ]); // uses DB::bind() for each entry\n```\n```php\n$db = DB::prepare(\"SELECT title FROM books WHERE author = ? AND published = ?\")\n    -\u003ebindMultiple(['John Doe', true]); // uses DB::bindPlaceholder() for each entry,\n                                        // when detects an array with key `0`\n```\n\n`DB::execute(?array $relations = null) : bool`\n\n```php\nDB::beginTransaction();\n\n$result = DB::prepare(\"INSERT INTO books (title, author, published)\n                       VALUES (:title, :author, :published)\")\n    -\u003ebind('title', 'The Cool Title') // -\u003ebind() still applicable\n    -\u003eexecute([\n        ':author' =\u003e 'John Doe',\n        ':published' =\u003e false,\n    ]); // passes optional $relations to DB::bindMultiple() and then executes\n\nif ($result) {\n    $bookId = DB::lastInsertId();\n    // ...\n} else {\n    DB::rollback();\n    // handle the error ...\n}\n\nDB::commit();\n```\n```php\n$result = DB::prepare(\"DELETE FROM books WHERE title = ?\")\n    -\u003eexecute(['The Cool Title']);\n\nif (DB::rowCount()) {\n    // Successfully deleted...\n} else {\n    // No rows affected...\n}\n```\n\n`DB::single(?array $relations = null) : ?array`\n\n```php\n$result = DB::prepare(\"SELECT title FROM books WHERE author = ? AND published = ?\")\n    -\u003esingle(['John Doe', true]);\n\nif ($result) {\n    $title = $result['title'];\n    // ...\n} else {\n    // No result, or error ...\n}\n```\n\n`DB::resultset(?array $relations = null) : array`\n\n```php\n$rows = DB::prepare(\"SELECT title FROM books WHERE author = ? AND published = ?\")\n    -\u003eresultset(['John Doe', true]);\n\nif (count($rows)) {\n    // ...\n} else {\n    // No result\n}\n```\n\n`DB::generatePlaceholders(int $count) : string`\n\nHelper method to generate question marks to include into a query.\n\n```php\n$placeholders = DB::generatePlaceholders(4); // '?,?,?,?'\n```\n\n`DB::dateTime(?int $timestamp = null) : string`\n\nReturns a dateTime string matching the ISO 8601 \"YYYY-MM-DD hh:mm:ss\" format.\n\n```php\n$dateTime = DB::dateTime(1735689600); // '2025-01-01 00:00:00'\n$dateTimeNow = DB::dateTime(); // time now (UTC time zone by default)\n```\n\n`DB::close() : void`\n\nCloses the database connection. The framework closes the connection automatically on API response, so it's not mandatory to use this method.\n\n\n### HTTP `Val\\App\\HTTP`\n\n#### Usage\n\n`HTTP::get(string $url, array $parameters = []) : ?array`\n\n```php\n$url = 'https://api.example.com/books'; // do not attach \"?params\"\n$params = [\n    'id'   =\u003e 123,\n    'sort' =\u003e 'year'\n]\n\n$result = HTTP::get($url, $params); // tries to JSON decode the response, may return null\n```\n\n`HTTP::post(string $url, array $parameters = []) : ?array`\n\n```php\n$url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';\n$params = [\n    'secret'   =\u003e 'very-secret',\n    'response' =\u003e 'user-response',\n    'remoteip' =\u003e Auth::getIPAddress()\n];\n\n$result = HTTP::post($url, $params); // tries to JSON decode the response, may return null\n```\n\n\n### JSON `Val\\App\\JSON`\n\n#### Usage\n\n`JSON::encode(array $data) : ?string`\n\n```php\n$data = [\n    'name' =\u003e \"John Doe\",\n    'age'  =\u003e 40\n];\n\n$json = JSON::encode($data); // {\"name\":\"John Doe\",\"age\":40}\n```\n\n`JSON::decode(?string $json) : ?array`\n\n```php\n$data = JSON::decode($json); // may return null in case of error\n```\n\n\n### Lang `Val\\App\\Lang`\n\nThe Lang module detects the user preferred language, or sets a specific language for the user. Optionally, manages the language code in the URL path.\n\nLanguage code format: `\u003cISO 639 language code\u003e[-\u003cISO 3166-1 region code\u003e]`\n\n#### Configs `config/app.php` (optional)\n\n- (optional) `languages =\u003e ['fr', 'en', ...]` – a list of supported languages (e.g. if not specified in the list, the detected `en-US` will fallback to `en`).\n- (optional) `language_in_url =\u003e true|false` – whether to manage the language code in the URL path.\n\n#### Detection precedence\n\n1. From `lang` cookie (which was set previously, if it's not the first user's request).\n2. From 'Accept-Language' header.\n3. Default supported language (the first one in the `Config::app('languages')` list), if the list is set.\n\n#### Usage\n\n`Lang::get() : ?string`\n\nReturns the detected language code, or null if the language couldn't be detected.\n\n```php\n$lang = Lang::get(); // 'en'\n```\n\n`Lang::set(string $code) : bool`\n\nReturns false, if the language code has an invalid format or an error occurred while setting the `lang` cookie, otherwise true.\n\n```php\n// Unsets the language, or if the supported languages list is specified, sets \n// to the first one in the list.\n$result = Lang::set('');\n$lang = Lang::get(); // 'fr'\n\n// Set the language to \"English (United States)\".\n$result = Lang::set('en-US');\n```\n\n\n\n### Renderer `Val\\App\\Renderer`\n\nRenderer module allows to:\n\n- Load a template file with a custom extenstion (.tpl, .html, ...).\n- Minify the loaded template by removing the following characters: `[\\r\\n\\t] 1+`, `\u003cspace\u003e 2+`, `HTML comments`\n- Insert other template's content, e.g. `{@header/nav.html}`.\n- Bind data to placeholders, e.g. `{title}`, `{content}`.\n- Reveal blocks (the blocks are removed from the result if not \"revealed\"), e.g. `[in_stock]Product in stock![/in_stock]`.\n\nFor convenience, static methods of the `Renderer` class return an instance of this class (singleton), so the \"template-processing\" methods can be further chained using the `-\u003e` operator.\n\n#### Example templates\n\n```html\n\u003c!-- view/main.tpl --\u003e\n{@subdir/greeting.tpl}\n{@reference-to-an-inexisting-template.tpl}\n\n[reveal_me]This will be shown.[/reveal_me]\n[block_name]This will be removed.[/block_name]\n\nGood to know:\n- Unregistered {binds} are not removed.\n- [wrong] A block must have the same start and end tag. [/block]\n```\n\n```html\n\u003c!-- view/subdir/greeting.tpl --\u003e\nHello, {greeting}!\n```\n#### Usage\n\n`Renderer::setPath(string $directoryPath) : self`\n\n```php\n// Example setting the path to a custom templates directory.\n// By default, the path is `App::$DIR_VIEW` (meaning the \"view/\" directory).\nRenderer::setPath(App::$DIR_ROOT . '/mytemplates');\n```\n\n`Renderer::load(string $file, bool $minify = true) : self`\n\n```php\n// In public/index.php\nApp::run(function() {\n    $content = Renderer::load('main.tpl', false)-\u003egetContent();\n    echo $content;\n}\n```\nResult:\n```html\n\u003c!-- view/main.tpl --\u003e\nHello, {greeting}!\n{@reference-to-an-inexisting-template.tpl}\n\n\n\n\nGood to know:\n- Unregistered {binds} are not removed.\n- [wrong] A block must have the same start and end tag. [/block]\n```\n\n`Renderer::bind(string $binding, string $value = '') : self`\n\n```php\n$content = Renderer::load('main.tpl')\n    -\u003ebind('greeting', 'World')\n    -\u003egetContent();\n```\n```html\n...\nHello, World!\n...\n- Unregistered {binds} are not removed.\n...\n```\n\n`Renderer::bindMultiple(array $relations) : self`\n\n```php\n$content = Renderer::load('main.tpl')\n    -\u003ebindMultiple([\n        'greeting' =\u003e 'World',\n        'binds' =\u003e 'Binds',\n    ])\n    -\u003egetContent();\n```\n\n`Renderer::reveal(string $block) : self`\n\n```php\n$content = Renderer::load('main.tpl')\n    -\u003ereveal('reveal_me')\n    -\u003egetContent();\n```\n```html\n...\nThis will be shown.\n...\n- [wrong] A block must have the same start and end tag. [/block]\n```\n\n`Renderer::revealMultiple(array $blocks) : self`\n\n```php\n$content = Renderer::load('main.tpl')\n    -\u003erevealMultiple([\n        'reveal_me',\n        'block_name'\n    ])\n    -\u003egetContent();\n```\n\n`Renderer::getContent() : string`\n\nReturns the rendered content of the loaded template.\n```php\n$content1 = Renderer::load('index.tpl')-\u003egetContent();\n$content2 = Renderer::load('email.tpl')-\u003egetContent();\n```\n\n### Token `Val\\App\\Token`\n\nToken module helps to manage custom tokens which represent some data encoded in JSON format and then  encrypted by using the application key. Useful for encrypting and storing custom data in the cookies or local storage.\n\n#### Configs `config/app.php` (required)\n\n- (required) `key =\u003e 'app-key'` – required for encryption.\n\n#### Usage\n\n`Token::create(array $data) : ?string`\n\n```php\n$testData = [\n    'testId' =\u003e 2,\n    'score'  =\u003e 94,\n    'username' =\u003e 'john_doe',\n    'testPassedAt' =\u003e DB::dateTime()\n];\n\n$token = Token::create($testData);\n\n// Storing the token\n// ...\n```\n\n`Token::extract(string $token) : ?array`\n\n```php\n// Retrieving the token\n// ...\n\n$testData = Token::extract($token);\n```\n\n`Token::expired(string $createdAt, int $timeToLive, string $timeScale) : bool`\n\nChecks if a token has expired based on creation time and time to live (TTL). The time scale for TTL must be specified using one of the class constants: `Token::TIME_SECONDS`, `Token::TIME_MINUTES`, `Token::TIME_HOURS`, `Token::TIME_DAYS`.\n\n```php\n$shouldTakeTest = Token::expired($testData['testPassedAt'], 30, Token::TIME_DAYS);\n```\n\n### UUID `Val\\App\\UUID`\n\n#### Usage\n\n`UUID::generate() : ?string`\n\n```php\n// Generating a UUID Version 7 (RFC 9562).\n$uuid = UUID::generate(); // may return `null`\n```\n\n## API-related modules\n\n`Val\\Api\\{...}`\n\nThe API-related modules are used primarily in `api/` classes.\n\n- [Two-Factor authentication](#twofactorauth)\n- [Captcha](#captcha)\n- [Mail](#mail)\n\n### TwoFactorAuth `Val\\Api\\TwoFactorAuth`\n\n#### Usage\n\n`TwoFactorAuth::generateSecretKey() : ?string`\n\n```php\n/**\n * Generating a TOTP (Time-based one-time password) secret key for the user.\n */\n$secretKey = TwoFactorAuth::generateSecretKey();\n\n/**\n * Storing user's TOTP secret key securely in the database.\n */\n$encryptedSecretKey = Crypt::encrypt($secretKey);\n// [...]\n```\n\n`TwoFactorAuth::createURI(string $secretKey, string $appName, string $accountName) : string`\n\n```php\n/**\n * Generating an URI that may be further sent to the frontend and encoded into \n * a QR code, so the user can scan it with his favorite Authenticator app.\n */\n$appName = 'My App'; // your app's name, e.g. 'Example.com', 'app', ...\n$accountName = 'john@doe.com'; // user's account name, e.g. 'john_doe', 'John Doe', ...\n$URI = TwoFactorAuth::createURI($secretKey, $appName, $accountName);\n\n```\n\n`TwoFactorAuth::verify(string $secretKey, string $code) : bool`\n\n```php\n/**\n * Later on, the user enters the code generated by his Authenticator app.\n * Getting the user's secret key from the database and veryfing the code.\n */\n// [...]\n$secretKey = Crypt::decrypt($encryptedSecretKey); // may return `null`\n$code = $this-\u003eval('code');\n$result = TwoFactorAuth::verify($secretKey, $code); // returns `true` if the code is correct\n```\n\n### Captcha `Val\\Api\\Captcha`\n\n#### Usage\n\n#### [Turnstile (Cloudflare)](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)\n\n`Captcha::Turnstile(string $secret, string $response) : ?array`\n\n```php\n$secret = '\u003cSECRET\u003e'; // your Turnstile secret key\n$response = '\u003cCLIENT-RESPONSE\u003e'; // response token from the frontend\n$result = Captcha::Turnstile($secret, $response); // makes an HTTP request, may return `null`\n```\n\n#### [hCaptcha (Intuition Machines)](https://docs.hcaptcha.com/#verify-the-user-response-server-side)\n\n`Captcha::hCaptcha(string $secret, string $response, ?string $sitekey = null) : ?array`\n\nExample using `config/app.php` and `config/env.php` for storing the secret key.\n\n```php\n// In config/env.php ...\n'hcaptcha_secret' =\u003e '\u003cSECRET\u003e',\n\n// In config/app.php ...\n'hcaptcha_secret' =\u003e Config::env('hcaptcha_secret'),\n\n// Somewhere in your API's method ...\n$secret = Config::app('hcaptcha_secret');\n$response = $this-\u003eval('hcaptcha_response');\n$sitekey = 'optional-site-key';\n$result = Captcha::hCaptcha($secret, $response, $sitekey);\n```\n\n#### [reCAPTCHA (Google)](https://developers.google.com/recaptcha/docs/v3#site_verify_response)\n\n`Captcha::reCAPTCHA(string $secret, string $response) : ?array`\n\n```php\n$result = Captcha::reCAPTCHA($secret, $response);\n```\n\n### Mail `Val\\Api\\Mail`\n\nMail module uses the standard `mail()` function that provides the very basic functionality. In a real application, it is recommended to use any popular library for email sending.\n\n#### Usage\n\n`Mail::send(array $options) : bool`\n\n```php\n$options = [\n    'from' =\u003e ['name' =\u003e \"Company name\", 'address' =\u003e \"email@company.com\"],\n    'to'   =\u003e [\"User name\" =\u003e \"email@user1.com\", \"email@user2.com\"],\n    'cc'   =\u003e [\"User name\" =\u003e \"email@user1.com\", \"email@user2.com\"],\n    'bcc'  =\u003e [\"User name\" =\u003e \"email@user1.com\", \"email@user2.com\"],\n    'subject'          =\u003e \"The subject\",\n    'messageHTML'      =\u003e \"\u003cp\u003eHello!\u003c/p\u003e\",\n    'messagePlainText' =\u003e \"Hello!\"\n];\n\n$result = Mail::send($options); // returns `true` on success\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freves%2Fval","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freves%2Fval","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freves%2Fval/lists"}