├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── docs ├── Database.md ├── README.md ├── Request.md ├── Response.md ├── Router.md ├── Validator.md └── View.md ├── phpcs.xml ├── phpstan.neon └── src ├── Cache.php ├── Config.php ├── Console.php ├── Container.php ├── Crypt.php ├── DB.php ├── Error.php ├── Event.php ├── Exceptions ├── ContainerException.php ├── CurlException.php ├── DataNotFoundException.php ├── EventException.php ├── JwtException.php ├── MikroException.php ├── PathException.php ├── ValidatorException.php └── ViewException.php ├── Helper.php ├── Jwt.php ├── Locale.php ├── Logger.php ├── Request.php ├── Response.php ├── Router.php ├── Validator.php └── View.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `mikro` will be documented in this file 4 | 5 | ## 1.1.0 - 2022-03-01 6 | - Exceptions added 7 | - `dont_report` method added to Error component 8 | - Fix on CurlError exception message 9 | - `isServerError`, `isClientError`, `isFailed`, `isOk`, `isRedirect` and `errorOnFail` methods added to Curl component 10 | - Some PHP 8 improvements 11 | 12 | ## 1.0.0 - 2022-10-01 13 | - initial release 14 | - It is not compatible with the previous version. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Yılmaz Demir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project in development. **Do not use** (yet) 2 | 3 | # mikro - micro approach to traditional 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/yidemir/mikro.svg?style=flat-square)](https://packagist.org/packages/yidemir/mikro) [![Total Downloads](https://img.shields.io/packagist/dt/yidemir/mikro.svg?style=flat-square)](https://packagist.org/packages/yidemir/mikro) [![License](https://img.shields.io/packagist/l/yidemir/mikro)](https://packagist.org/packages/yidemir/mikro) 6 | 7 | This project is a tool developed to solve some tasks and requests with simple methods, rather than a framework. 8 | 9 | I tried to take this project, which I started as a hobby, one step further. There have been fundamental changes compared to the previous version. 10 | 11 | Available packages: 12 | * **Cache** - It is a simple caching structure. 13 | * **Config** - It is a simple config structure with setter and getter. 14 | * **Console** - Executes a callback according to the parameter from the command line. 15 | * **Container** - A simple service container. 16 | * **Crypt** - It encrypts and decrypts strings with OpenSSL. 17 | * **DB** - It simplifies your CRUD operations with a PDO instance. 18 | * **Event** - A simple event listener and emitter. 19 | * **Helper** - String and array helpers and more 20 | * **Jwt** - A simple JSON web token authentication structure. 21 | * **Locale** - Multi-language/localization structure 22 | * **Logger** - Basic logging 23 | * **Request** - An easy way to access PHP global request variables. 24 | * **Response** - Sends data/response to the client. 25 | * **Router** - An ultra-simple router with grouping and middleware support. 26 | * **Validator** - A simple data validation library. 27 | * **View** - A view renderer with block and template support. 28 | 29 | ## Installation 30 | 31 | You can install the package via composer: 32 | 33 | ```bash 34 | composer require yidemir/mikro 35 | ``` 36 | 37 | ## Usage 38 | 39 | **Routing** 40 | ``` php 41 | Router\get('/', fn() => Response\view('home')); 42 | ``` 43 | 44 | ```php 45 | Router\group('/admin', fn() => [ 46 | Router\get('/', 'DashboardController::index'), 47 | Router\resource('/posts', PostController::class), 48 | Router\get('/service/status', fn() => Response\json(['status' => true], 200) 49 | ], ['AdminMiddleware::handle']); 50 | 51 | Router\files('/', __DIR__ . '/sync-directory'); 52 | ``` 53 | 54 | ```php 55 | Router\error(fn() => Response\html('Default 404 error', 404)); 56 | ``` 57 | 58 | **Database** 59 | ```php 60 | $products = DB\query('select * from products order by id desc')->fetchAll(); 61 | $product = DB\query('select * from products where id=?', [$id])->fetch(); 62 | 63 | DB\insert('products', ['name' => $name, 'description' => $description]); 64 | $id = DB\last_insert_id(); 65 | 66 | DB\update('products', ['name' => $newName], 'where id=?', [$id]); 67 | DB\delete('products', 'where id=?', [$id]); 68 | ``` 69 | 70 | **View and Templates** 71 | ```html 72 | @View\set('title', 'Page title!'); 73 | 74 | @View\start('content'); 75 |

Secure print: @=$message; or unsecure print @echo $message;

76 | @View\stop(); 77 | 78 | @View\start('scripts'); 79 | 80 | @View\push(); 81 | 82 | @echo View\render('layout'); 83 | ``` 84 | 85 | ```html 86 | 87 | 88 | 89 | 90 | 91 | 92 | @View\get('title', 'Hey!'); 93 | 94 | 95 | @View\get('content'); 96 | 97 | @View\get('scripts'); 98 | 99 | 100 | ``` 101 | 102 | All methods and constants are documented at the source. The general documentation will be published soon. 103 | 104 | ### Testing 105 | 106 | ```bash 107 | composer test 108 | ``` 109 | 110 | ### Changelog 111 | 112 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 113 | 114 | ## Contributing 115 | 116 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 117 | 118 | ### Security 119 | 120 | If you discover any security related issues, please email demiriy@gmail.com instead of using the issue tracker. 121 | 122 | ## Credits 123 | 124 | - [Yılmaz Demir](https://github.com/yidemir) 125 | - [All Contributors](../../contributors) 126 | 127 | ## License 128 | 129 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 130 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yidemir/mikro", 3 | "description": "Micro approach to traditional", 4 | "keywords": [ 5 | "yidemir", 6 | "mikro", 7 | "framework", 8 | "php", 9 | "tool" 10 | ], 11 | "homepage": "https://github.com/yidemir/mikro", 12 | "license": "MIT", 13 | "type": "library", 14 | "authors": [ 15 | { 16 | "name": "Yılmaz Demir", 17 | "email": "demiriy@gmail.com", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.1", 23 | "ext-curl": "*", 24 | "ext-json": "*", 25 | "ext-mbstring": "*", 26 | "ext-openssl": "*", 27 | "ext-pdo": "*", 28 | "ext-readline": "*" 29 | }, 30 | "require-dev": { 31 | "phpstan/phpstan": "^1.2", 32 | "phpunit/phpunit": "^9.0" 33 | }, 34 | "autoload": { 35 | "files": [ 36 | "src/Cache.php", 37 | "src/Config.php", 38 | "src/Console.php", 39 | "src/Container.php", 40 | "src/Crypt.php", 41 | "src/DB.php", 42 | "src/Error.php", 43 | "src/Event.php", 44 | "src/Helper.php", 45 | "src/Jwt.php", 46 | "src/Locale.php", 47 | "src/Logger.php", 48 | "src/Request.php", 49 | "src/Response.php", 50 | "src/Router.php", 51 | "src/Validator.php", 52 | "src/View.php" 53 | ], 54 | "psr-4": { 55 | "Mikro\\Exceptions\\": "src/Exceptions" 56 | } 57 | }, 58 | "autoload-dev": { 59 | "psr-4": { 60 | "Mikro\\Tests\\": "tests" 61 | } 62 | }, 63 | "scripts": { 64 | "test": "vendor/bin/phpunit", 65 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 66 | "analyze": "vendor/bin/phpstan --memory-limit=2G" 67 | }, 68 | "config": { 69 | "sort-packages": true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/Database.md: -------------------------------------------------------------------------------- 1 | # Database 2 | 3 | Setup: 4 | ```php 5 | $mikro[DB\CONNECTION] = new PDO('connection-string'); 6 | ``` 7 | 8 | ## Query 9 | ```php 10 | $sth = DB\query('select * from items'); 11 | $items = $sth->fetchAll(); 12 | 13 | $sth = DB\query('select * from items where id=:id', ['id' => 5]); 14 | $item = $sth->fetch(); 15 | 16 | DB\query('insert into items (name, value) values (?, ?)', [$name, $value]); 17 | ``` 18 | 19 | ## Execute 20 | ```php 21 | DB\exec('create table if not exists items ...'); 22 | ``` 23 | 24 | ## Insert 25 | ```php 26 | DB\insert('items', ['name' => 'value']); 27 | ``` 28 | 29 | ## Update 30 | ```php 31 | DB\update('items', ['name' => 'foo', 'value' => 'bar']); 32 | DB\update('items', ['name' => 'foo', 'value' => 'bar'], 'where id=?', [$id]); 33 | ``` 34 | 35 | ## Delete 36 | ```php 37 | DB\delete('items'); 38 | DB\delete('items', 'where id=?', [$id]); 39 | ``` 40 | 41 | ## Last Insert ID 42 | ```php 43 | DB\insert('items', ['name' => 'value']); 44 | DB\last_insert_id(); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # mikro docs 2 | 3 | - [Router](Router.md) 4 | - [Basics](Router.md#basics) 5 | - [Parameters](Router.md#route-parameters) 6 | - [Groups](Router.md#route-groups) 7 | - [Middleware](Router.md#middleware) 8 | - [Handling 404](Router.md#handling-404) 9 | - [Request](Request.md) 10 | - [Response](Response.md) 11 | - [Database](Database.md) 12 | - [View](View.md) 13 | - [Basics](View.md#basics) 14 | - [Blocks](View.md#blocks) 15 | - [Templating](View.md#templating) 16 | -------------------------------------------------------------------------------- /docs/Request.md: -------------------------------------------------------------------------------- 1 | # Request 2 | Simple request handler 3 | 4 | ## Methods 5 | 6 | ### Method 7 | ```php 8 | Request\method(); // example result: 'GET' 9 | ``` 10 | 11 | ### Request Path 12 | ```php 13 | Request\path(); // example result: '/posts/5' 14 | ``` 15 | 16 | ### Request Query String 17 | ```php 18 | Request\query_string(); // example result: 'id=5&page=32' 19 | ``` 20 | 21 | ### Request Query 22 | ```php 23 | Request\query(); // example result: ['id' => 5, 'page' => 32] 24 | ``` 25 | 26 | ### Request Parameters 27 | ```php 28 | Request\all(); // example result: ['id' => 5, 'page' => 32] 29 | ``` 30 | 31 | ### Request Parameter 32 | ```php 33 | Request\get('page'); // example result: '32' 34 | // or 35 | Request\input('page'); // example result: '32' 36 | ``` 37 | 38 | ### Request Body 39 | Gets request body 40 | ```php 41 | Request\content(); // returns request body 42 | ``` 43 | 44 | ### Request Body (array) 45 | Gets request body in array 46 | ```php 47 | Request\to_array(); // array 48 | ``` 49 | 50 | ### Request Header 51 | ```php 52 | Request\headers(); // get all headers in array 53 | Request\header('Content-type'); // get Content-Type header 54 | ``` 55 | 56 | ### Check Json Request 57 | ```php 58 | Request\wants_json(); // bool 59 | ``` 60 | 61 | ### Bearer Token 62 | ```php 63 | Request\bearer_token(); // bool 64 | ``` 65 | 66 | ### Only specific parameters 67 | ```php 68 | Request\only(['param1', 'param2']); // array 69 | ``` 70 | 71 | ### Except specific parameters 72 | ```php 73 | Request\except(['param1', 'param2']); // array 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/Response.md: -------------------------------------------------------------------------------- 1 | # Response 2 | Response handler 3 | 4 | ## HTML Response 5 | Prints and returns HTML response 6 | ```php 7 | Response\html('...'); 8 | 9 | // Response\html(content: 'string', code: 200, headers: []): int 10 | ``` 11 | 12 | ## JSON Response 13 | Prints and returns JSON response 14 | ```php 15 | Response\json(['message' => 'OK']); 16 | 17 | // Response\json(content: ['message' => 'OK'], code: 200, headers: []): int 18 | ``` 19 | 20 | ## Plain-text Response 21 | Prints and returns plain text response 22 | ```php 23 | Response\text('plain text'); 24 | 25 | // Response\text(content: 'string', code: 200, headers: []): int 26 | ``` 27 | 28 | ## Rendered View Response 29 | ```php 30 | Response\view('view-file', ['parameter' => 'value']); 31 | 32 | // Response\view(file: 'view-file', data: [], code: 200, headers: []): int 33 | ``` 34 | 35 | ## Redirects 36 | ```php 37 | Response\redirect('/to-path'); 38 | Response\redirect('https://to-url.com'); 39 | Response\redirect_back(); 40 | ``` 41 | 42 | ## Success Response 43 | ```php 44 | Response\ok('html response'); 45 | Response\ok(['message' => 'json respnose']); 46 | 47 | // Response\ok(data: 'mixed', code: 200, headers: []): int 48 | ``` 49 | 50 | ## Error Response 51 | ```php 52 | Response\error('html response'); // 500 server error 53 | Response\error(['message' => 'json respnose']); // 500 server error 54 | 55 | Response\error('404 page not found', Response\STATUS['HTTP_NOT_FOUND']); 56 | Response\error(['message' => '404 not found'], 404); 57 | 58 | // Response\error(data: 'mixed', code: 500, headers: []): int 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/Router.md: -------------------------------------------------------------------------------- 1 | # Router 2 | Router is simple and powerful page handler. 3 | 4 | ## Basics 5 | Router is under the `Router` namespace. 6 | 7 | Router methods are: 8 | ```php 9 | Router\get('/', 'callback'); 10 | Router\post('/save', 'callback'); 11 | Router\patch('/update', 'callback'); 12 | Router\put('/update', 'callback'); 13 | Router\delete('/delete', 'callback'); 14 | Router\any('/page', 'callback'); 15 | Router\redirect('/page', '/to'); 16 | Router\view('/path', 'viewfile'); 17 | ``` 18 | 19 | The `callback` parameter must be contain a PHP [callable](https://www.php.net/manual/en/language.types.callable.php). 20 | 21 | ### File Based Routing 22 | Converts files in the path and directory specified using the `RecursiveDirectoryIterator` to the route. For example: 23 | 24 | ```php 25 | Router\files('/', __DIR__ . '/pages'); 26 | ``` 27 | 28 | | File Path | Router Equivalent | 29 | |--|--| 30 | | `/pages/index.php` | `Router\get('/', 'callback');` | 31 | | `/pages/filename.php` | `Router\get('/filename', 'callback');` | 32 | | `/pages/index.post.php` | `Router\post('/', 'callback');` | 33 | | `/pages/index.put.php` | `Router\put('/', 'callback');` | 34 | | `/pages/index.patch.php` | `Router\patch('/', 'callback');` | 35 | | `/pages/index.options.php` | `Router\options('/', 'callback');` | 36 | | `/pages/index.delete.php` | `Router\delete('/', 'callback');` | 37 | | `/pages/filename.post.php` | `Router\post('/filename', 'callback');` | 38 | | `/pages/with-{param}.php` | `Router\get('/with-{param}', 'callback');` | 39 | | `/pages/with-{param}.post.php` | `Router\post('/with-{param}', 'callback');` | 40 | | `/pages/items/index.php` | `Router\get('/items', 'callback');` | 41 | | `/pages/items/index.post.php` | `Router\post('/items', 'callback');` | 42 | | `/pages/items/{id}.php` | `Router\get('/items/{id}', 'callback');` | 43 | | `/pages/items/{id}.put.php` | `Router\put('/items/{id}', 'callback');` | 44 | 45 | ## Route Parameters 46 | The Router uses regular expressions to map the routes. If you don't want to use Regex strings, you can use patterns. 47 | 48 | ```php 49 | Router\get('/items/{name}', 'ItemController::show'); 50 | Router\put('/events/{id:num}', 'EventController::update'); 51 | Router\get('/posts/{slug:str}', 'PostController::show'); 52 | ``` 53 | 54 | Available patterns: 55 | ``` 56 | {parameter} => any string 57 | {parameter:num} => \d+ (digits) 58 | {parameter:str} => [\w\-_]+ (string) 59 | {parameter:any} => [^/]+ (any string, includes special chars, digits etc.) 60 | {parameter:all} => .* (any string on full uri) 61 | ``` 62 | 63 | ### Getting Route Parameters 64 | To use the resolved route parameters in the callback, you must use the `parameters()` method. For example: 65 | 66 | ```php 67 | Router\get('/posts/{slug}', function () { 68 | $slug = Router\parameters('slug'); 69 | }); 70 | ``` 71 | 72 | ## Route Groups 73 | Route Groups, provides grouping according to the specified prefix. 74 | 75 | ```php 76 | Router\get('/home', 'HomeController::index'); 77 | 78 | Router\group('/admin', function () { 79 | Router\get('', 'Admin\DashboardController::index'); 80 | Router\get('/stats', 'Admin\DashboardController::stats'); 81 | 82 | // if you want, you can use nested groups or arrow functions 83 | Router\group('/posts', fn() => [ 84 | Router\get('', 'Admin\PostController::index'), 85 | Router\get('/{id:num}', 'Admin\PostController::show'), 86 | Router\get('/create', 'Admin\PostController::create'), 87 | Router\post('', 'Admin\PostController::store'), 88 | ]); 89 | }); 90 | ``` 91 | 92 | ## Middleware 93 | The routes and groups includes a simple middleware support. 94 | 95 | **Usage** 96 | ```php 97 | Router\get('/', 'route_callback', ['middleware_callback']); 98 | ``` 99 | 100 | **Example** 101 | ```php 102 | function middleware(\Closure $next) 103 | { 104 | // things 105 | 106 | return $next(); 107 | } 108 | 109 | Router\get('/', 'callback', ['middleware']); 110 | ``` 111 | 112 | ### Route Group Middleware 113 | ```php 114 | Router\group('/admin', fn() => [ 115 | Router\get('/dashboard', 'DashboardController::index', ['DashboardMiddleware::handle']) 116 | ], ['AdminAuthenticationMiddleware::handle']); 117 | ``` 118 | 119 | ## Handle 404 120 | It should be located at the end of the route definitions. 121 | 122 | **Usage** 123 | ```php 124 | Router\error(function () { 125 | return Response\html('404 page not found'); 126 | }); 127 | ``` 128 | 129 | -------------------------------------------------------------------------------- /docs/Validator.md: -------------------------------------------------------------------------------- 1 | # Validator 2 | Simple array validator 3 | 4 | ## Validation Method 5 | Validator performs validation using PHP callbacks. For example: 6 | 7 | ```php 8 | $data = ['key' => 'value', 'foo' => 'bar', 'baz' => '']; 9 | 10 | Validator\validate($data, 'key', 'isset|!empty'); // returns true, its valid 11 | Validator\validate($data, 'key', ['isset', '!empty']); // returns true, its valid 12 | Validator\validate($data, 'qux', 'isset|!empty'); // returns false, qux key not exists 13 | Validator\validate($data, 'qux', '!empty'); // returns false, qux key not exists 14 | Validator\validate($data, 'baz', '!empty'); // returns false, baz key is empty 15 | Validator\validate($data, 'baz', fn($value, $data) => strlen($value) >= 3); 16 | Validator\validate($data, 'baz', [fn($value, $data) => strlen($value) >= 3]); 17 | 18 | $callback = fn($value, $data) => true; 19 | Validator\valdiate(Request\all(), 'username', [$callback, 'ctype_alnum']); 20 | ``` 21 | 22 | **Validate with parameters** 23 | **** 24 | ```php 25 | Validator\validate(Request\all(), 'email', ['isset', '!empty', 'filter_var:' . FILTER_VALIDATE_EMAIL]); 26 | ``` 27 | 28 | **Validate and get all results in array** 29 | ```php 30 | Validator\validate_all( 31 | ['title' => 'foo', 'email' => 'bar'], 32 | [ 33 | 'title' => 'isset|!empty|ctype_alnum', 34 | 'email' => 'isset|!empty|filter_var:' . FILTER_VALIDATE_EMAIL 35 | ] 36 | ); 37 | 38 | // array(2) { 39 | // ["title"]=> bool(true) 40 | // ["email"]=> bool(false) 41 | // } 42 | ``` 43 | 44 | ```php 45 | Validator\is_validate_all( 46 | ['title' => 'foo', 'email' => 'bar'], 47 | [ 48 | 'title' => 'isset|!empty|ctype_alnum', 49 | 'email' => 'isset|!empty|filter_var:' . FILTER_VALIDATE_EMAIL 50 | ] 51 | ); // returns bool 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/View.md: -------------------------------------------------------------------------------- 1 | # View 2 | Simple view handler 3 | 4 | ## Rendering View File 5 | ```php 6 | View\render('view-file', ['data' => 'value']); // returns string 7 | ``` 8 | 9 | ## View Blocks 10 | **Set view block:** 11 | ```php 12 | View\start('content'); 13 | echo '

Content

'; 14 | View\stop(); 15 | 16 | // or 17 | 18 | View\set('content', '

Content

'); 19 | ``` 20 | 21 | **Call view block:** 22 | ```php 23 | echo View\get('content'); 24 | ``` 25 | 26 | ## Example 27 | 28 | **index.php** 29 | ```php 30 | $mikro[View\PATH] = __DIR__ . '/views'; 31 | 32 | View\render('index', ['message' => 'Hello world!']); 33 | ``` 34 | 35 | **views/layout.php** 36 | ```php 37 | 38 | 39 | 40 | 41 | 42 | <?= View\get('title', 'Default Title') ?> 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | 50 | **views/index.php** 51 | ```php 52 | 53 | 54 | 55 |

Message: 56 | 57 | 58 | 59 | ``` 60 | 61 | ## View Templates 62 | 63 | ```php 64 | @echo 'simple php block'; 65 | @='print secure string'; 66 | ``` 67 | 68 | rendered to: 69 | 70 | ```php 71 | 72 | 73 | ``` 74 | 75 | another example: 76 | 77 | ```php 78 | @$data = [1, 2, 3, 4, 5]; 79 | @foreach ($data as $number):; 80 | Number is: @=$number;
81 | @endforeach; 82 | ``` 83 | 84 | rendered to: 85 | 86 | ```php 87 | 88 | 89 | Number is:
90 | 91 | ``` 92 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mikro PHPCS configuration file 4 | src 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - src 4 | level: 6 5 | checkMissingIterableValueType: false 6 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | get($key); 45 | 46 | return \Memcached::RES_NOTFOUND === $memcached->getResultCode() ? 47 | null : $data; 48 | } 49 | 50 | if (! has($key)) { 51 | return null; 52 | } 53 | 54 | return \unserialize(\file_get_contents(path($key))); 55 | } 56 | 57 | /** 58 | * Defines a new cache item 59 | * 60 | * {@inheritDoc} **Example:** 61 | * ```php 62 | * Cache\set('items', $itemsData); 63 | * ``` 64 | * 65 | * @throws PathException if cache path is not writeable 66 | */ 67 | function set(string $key, mixed $data, int $ttl = 0): void 68 | { 69 | if ($memcached = memcached()) { 70 | $memcached->set($key, $data, $ttl); 71 | 72 | return; 73 | } 74 | 75 | if (! \is_writable($dirname = \dirname(path($key)))) { 76 | throw new PathException(\sprintf('Cache path not writable: %s', $dirname)); 77 | } 78 | 79 | \file_put_contents(path($key), \serialize($data)); 80 | } 81 | 82 | /** 83 | * Checks whether the cache item is defined 84 | * 85 | * {@inheritDoc} **Example:** 86 | * ```php 87 | * Cache\has('items'); // returns bool 88 | * ``` 89 | */ 90 | function has(string $key): bool 91 | { 92 | if ($memcached = memcached()) { 93 | $memcached->get($key); 94 | 95 | return \Memcached::RES_NOTFOUND !== $memcached->getResultCode(); 96 | } 97 | 98 | return \is_readable(path($key)); 99 | } 100 | 101 | /** 102 | * Deletes a defined cache item 103 | 104 | * {@inheritDoc} **Example:** 105 | * ```php 106 | * Cache\remove('items'); 107 | * ``` 108 | */ 109 | function remove(string $key): void 110 | { 111 | if ($memcached = memcached()) { 112 | $memcached->delete($key); 113 | 114 | return; 115 | } 116 | 117 | if (! \is_writable(path())) { 118 | throw new PathException(\sprintf('Cache path not writable: %s', path())); 119 | } 120 | 121 | if (! has($key)) { 122 | return; 123 | } 124 | 125 | \unlink(path($key)); 126 | } 127 | 128 | /** 129 | * Deletes all defined cache items 130 | * 131 | * {@inheritDoc} **Example:** 132 | * ```php 133 | * Cache\flush(); 134 | * ``` 135 | * 136 | * @throws PathException if cache path is not writable 137 | */ 138 | function flush(): void 139 | { 140 | if ($memcached = memcached()) { 141 | $memcached->flush(); 142 | 143 | return; 144 | } 145 | 146 | if (! \is_writable(path())) { 147 | throw new PathException(\sprintf('Cache path not writable: %s', path())); 148 | } 149 | 150 | foreach (\glob(path() . '/*.cache') as $file) { 151 | \unlink($file); 152 | } 153 | } 154 | 155 | /** 156 | * Returns memcached instance before checks 157 | * 158 | * {@inheritDoc} **Example:** 159 | * ```php 160 | * $mikro[Cache\DRIVER] = 'memcached'; 161 | * $memcached = new Memcached(); 162 | * $memcached->addServer('localhost', 11211); 163 | * Container\set(Memcached::class, $memcached); 164 | * 165 | * Cache\memcached(); // Memcached instance 166 | * ``` 167 | * 168 | * @throws MikroException if memcached not loaded 169 | */ 170 | function memcached(): \Memcached|bool 171 | { 172 | global $mikro; 173 | 174 | $driver = $mikro[DRIVER] ?? null; 175 | 176 | if ($driver !== 'memcached') { 177 | return false; 178 | } 179 | 180 | if (! \extension_loaded('memcached')) { 181 | throw new MikroException('Memcached extension is not available, please install'); 182 | } 183 | 184 | if (! Container\has(\Memcached::class)) { 185 | throw new MikroException( 186 | 'In order to use the Memcached driver, you must define the Memcache driver in the Container' 187 | ); 188 | } 189 | 190 | return Container\get(\Memcached::class); 191 | } 192 | 193 | /** 194 | * Remember cache with callback 195 | * 196 | * {@inheritDoc} **Example:** 197 | * ```php 198 | * $data = Cache\remember('posts', fn() => DB\query('...')->fetchAll(), 60); 199 | * ``` 200 | */ 201 | function remember(string $key, \Closure $callback, int $ttl = 0): mixed 202 | { 203 | if (has($key)) { 204 | return get($key); 205 | } else { 206 | set($key, $cache = $callback(), $ttl); 207 | 208 | return $cache; 209 | } 210 | } 211 | 212 | /** 213 | * Cache path constant 214 | * 215 | * {@inheritDoc} **Example:** 216 | * ```php 217 | * $mikro[Cache\PATH] = '/path/to/cache'; 218 | * ``` 219 | */ 220 | const PATH = 'Cache\PATH'; 221 | 222 | /** 223 | * Cache path constant 224 | * 225 | * {@inheritDoc} **Example:** 226 | * ```php 227 | * $mikro[Cache\DRIVER] = 'file'; 228 | * // default driver is 'file' 229 | * // available drivers: file, memcached 230 | * ``` 231 | */ 232 | const DRIVER = 'Cache\DRIVER'; 233 | } 234 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | [$key => $value], 22 | $value 23 | ); 24 | 25 | $mikro[COLLECTION] = \array_replace_recursive($mikro[COLLECTION] ?? [], $replace); 26 | } 27 | 28 | /** 29 | * Returns the configuration value 30 | * 31 | * {@inheritDoc} **Example:** 32 | * ```php 33 | * Config\get('foo.bar'); 34 | * Config\get('foo')['bar']; 35 | * ``` 36 | */ 37 | function get(string $key, mixed $default = null): mixed 38 | { 39 | global $mikro; 40 | 41 | if (\array_key_exists($key, $mikro[COLLECTION] ?? [])) { 42 | return $mikro[COLLECTION][$key]; 43 | } 44 | 45 | return \array_reduce( 46 | \explode('.', $key), 47 | fn($config, $key) => $config[$key] ?? $default, 48 | $mikro[COLLECTION] ?? [] 49 | ) ?? $default; 50 | } 51 | 52 | /** 53 | * Config collection constant 54 | * 55 | * @internal 56 | */ 57 | const COLLECTION = 'Config\COLLECTION'; 58 | } 59 | -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | Cache\flush()); 13 | * // php console.php cache:clear 14 | * 15 | * Console\command('say:hello', function (array $args) { 16 | * if (isset($args[0])) { 17 | * return Console\write("Hello {$args[0]}!"); 18 | * } 19 | * 20 | * Console\write('Hello world!'); 21 | * }); 22 | * 23 | * // php console.php say:hello // prints "Hello world!" 24 | * // php console.php say:hello Foo // prints "Hello Foo!" 25 | * ``` 26 | */ 27 | function command(string $name, callable $callback): mixed 28 | { 29 | global $argv; 30 | 31 | if (\PHP_SAPI === 'cli' && isset($argv[1]) && $argv[1] === $name) { 32 | $args = \array_values(\array_slice($argv, 2)); 33 | 34 | foreach ($args as $arg) { 35 | if (\str_starts_with($arg, '-')) { 36 | $pieces = \explode('=', \preg_replace('/^-{1,2}(\w+)/', '$1', $arg)); 37 | 38 | if (\str_starts_with($arg, '--')) { 39 | $args[$pieces[0]] = $pieces[1] ?? false; 40 | } else { 41 | foreach (\str_split($pieces[0]) as $letter) { 42 | $args[$letter] = false; 43 | } 44 | } 45 | } 46 | } 47 | 48 | return \call_user_func_array($callback, [$args]); 49 | } 50 | 51 | return null; 52 | } 53 | 54 | /** 55 | * Write command line 56 | * 57 | * {@inheritDoc} **Example:** 58 | * ```php 59 | * Console\write('message'); 60 | * ``` 61 | */ 62 | function write(string $str): int 63 | { 64 | return print($str . "\n"); 65 | } 66 | 67 | /** 68 | * Write command line (green color) 69 | * 70 | * {@inheritDoc} **Example:** 71 | * ```php 72 | * Console\info('message'); 73 | * ``` 74 | */ 75 | function info(string $str): int 76 | { 77 | return write("\e[0;32m{$str}\e[0m"); 78 | } 79 | 80 | /** 81 | * Write command line (red color) 82 | * 83 | * {@inheritDoc} **Example:** 84 | * ```php 85 | * Console\error('message'); 86 | * ``` 87 | */ 88 | function error(string $str): int 89 | { 90 | return write("\e[0;31m{$str}\e[0m"); 91 | } 92 | 93 | /** 94 | * Performs operations according to the input 95 | * 96 | * {@inheritDoc} **Example:** 97 | * ```php 98 | * Console\ask('How old are you?', function ($age) { 99 | * Console\info("You are $age"); 100 | * }); 101 | * ``` 102 | */ 103 | function ask(string $question, ?callable $callback = null): mixed 104 | { 105 | write($question); 106 | $line = \readline(); 107 | 108 | if ($callback) { 109 | \call_user_func_array($callback, [$line]); 110 | } 111 | 112 | return $line; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | new stdClass()) 16 | * ``` 17 | */ 18 | function set(string $name, mixed $value): void 19 | { 20 | global $mikro; 21 | 22 | $mikro[COLLECTION][$name] = $value; 23 | } 24 | 25 | /** 26 | * Returns the defined container item 27 | * 28 | * {@inheritDoc} **Example:** 29 | * ```php 30 | * Container\get('variable'); // 'string' 31 | * Container\get('closure'); // Closure 32 | * ``` 33 | */ 34 | function get(string $name): mixed 35 | { 36 | if (! has($name)) { 37 | throw new ContainerException('Container item not exists'); 38 | } 39 | 40 | global $mikro; 41 | 42 | return $mikro[COLLECTION][$name]; 43 | } 44 | 45 | /** 46 | * Returns the value of the defined container item 47 | * 48 | * {@inheritDoc} **Example:** 49 | * ```php 50 | * Container\value('closure'); // stdClass 51 | * ``` 52 | * 53 | * @throws ContainerException If the value is not callable 54 | */ 55 | function value(string $name, array $args = []): mixed 56 | { 57 | if (! \is_callable($value = get($name))) { 58 | throw new ContainerException('Value is not callable'); 59 | } 60 | 61 | return \call_user_func_array($value, $args); 62 | } 63 | 64 | /** 65 | * Defines a new container item with singleton pattern 66 | * 67 | * {@inheritDoc} **Example:** 68 | * ```php 69 | * Container\set('closure', fn() => new stdClass()); 70 | * Container\value('closure') === Container\value('closure'); // true 71 | * ``` 72 | */ 73 | function singleton(string $name, callable $callback): void 74 | { 75 | set($name, function () use ($callback) { 76 | static $object; 77 | 78 | if ($object === null) { 79 | $object = \call_user_func($callback); 80 | } 81 | 82 | return $object; 83 | }); 84 | } 85 | 86 | /** 87 | * Checks whether the container item is defined 88 | * 89 | * {@inheritDoc} **Example:** 90 | * ```php 91 | * Container\has('item'); // true or false 92 | * ``` 93 | */ 94 | function has(string $name): bool 95 | { 96 | global $mikro; 97 | 98 | return \array_key_exists($name, $mikro[COLLECTION] ?? []); 99 | } 100 | 101 | /** 102 | * Container collection constant 103 | * 104 | * @internal 105 | */ 106 | const COLLECTION = 'Container\COLLECTION'; 107 | } 108 | -------------------------------------------------------------------------------- /src/Crypt.php: -------------------------------------------------------------------------------- 1 | fetchAll(); 36 | * DB\query('select * from items where id=?', [$id])->fetch(); 37 | * DB\query('insert into items (name, value) values (?, ?)', [$name, $value]); 38 | * ``` 39 | */ 40 | function query(string $query, array $params = []): \PDOStatement 41 | { 42 | $sth = connection()->prepare($query); 43 | $sth->execute($params); 44 | 45 | return $sth; 46 | } 47 | 48 | /** 49 | * Executes an SQL query 50 | * 51 | * {@inheritDoc} **Example:** 52 | * ```php 53 | * DB\exec('create table if not exists items ...'); 54 | * ``` 55 | */ 56 | function exec(string $query): int|bool 57 | { 58 | return connection()->exec($query); 59 | } 60 | 61 | /** 62 | * Inserts data to the specified table 63 | * 64 | * {@inheritDoc} **Example:** 65 | * ```php 66 | * DB\insert('items', ['name' => 'foo', 'value' => 'bar']); 67 | * ``` 68 | */ 69 | function insert(string $table, array $data, string $queryPart = ''): \PDOStatement 70 | { 71 | $query = "INSERT INTO {$table} "; 72 | $query .= querify($data, 'insert'); 73 | $query .= empty($queryPart) ? '' : " {$queryPart}"; 74 | 75 | return query($query, \array_values($data)); 76 | } 77 | 78 | /** 79 | * Updates the data(s) in the specified table 80 | * 81 | * {@inheritDoc} **Example:** 82 | * ```php 83 | * DB\update('items', ['name' => 'foo', 'value' => 'bar']); 84 | * DB\update('items', ['name' => 'foo', 'value' => 'bar'], 'where id=?', [$id]); 85 | * ``` 86 | */ 87 | function update( 88 | string $table, 89 | array $data, 90 | string $query = '', 91 | array $parameters = [] 92 | ): \PDOStatement { 93 | $query = "UPDATE {$table} SET " . querify($data, 'update') . " {$query}"; 94 | $params = \array_values($data); 95 | 96 | if (! empty($parameters)) { 97 | $params = \array_merge($params, $parameters); 98 | } 99 | 100 | return query($query, $params); 101 | } 102 | 103 | /** 104 | * Deletes the data(s) from the specified table 105 | * 106 | * {@inheritDoc} **Example:** 107 | * ```php 108 | * DB\delete('items'); 109 | * DB\delete('items', 'where id=?', [$id]); 110 | * ``` 111 | */ 112 | function delete( 113 | string $table, 114 | string $query = '', 115 | array $parameters = [] 116 | ): \PDOStatement { 117 | $query = "DELETE FROM {$table} {$query}"; 118 | 119 | return query($query, $parameters); 120 | } 121 | 122 | /** 123 | * Get last insert id after insert query 124 | * 125 | * {@inheritDoc} **Example:** 126 | * ```php 127 | * DB\last_insert_id(); 128 | * ``` 129 | */ 130 | function last_insert_id(): int|string|bool 131 | { 132 | $id = connection()->lastInsertId(); 133 | 134 | if (\is_numeric($id)) { 135 | return (int) $id; 136 | } 137 | 138 | return $id; 139 | } 140 | 141 | /** 142 | * @internal 143 | */ 144 | function querify(array $data, string $type): string 145 | { 146 | $string = ''; 147 | 148 | switch ($type) { 149 | case 'insert': 150 | $arrayParameters = \array_values($data); 151 | $columnsString = \implode(',', \array_keys($data)); 152 | $valuesString = \implode(',', \array_fill(0, \count($arrayParameters), '?')); 153 | $string = "({$columnsString}) VALUES ({$valuesString})"; 154 | break; 155 | case 'update': 156 | foreach (\array_keys($data) as $key) { 157 | $string .= "{$key}=?,"; 158 | } 159 | 160 | $string = \rtrim($string, ','); 161 | break; 162 | } 163 | 164 | return $string; 165 | } 166 | 167 | /** 168 | * DB connection constant 169 | * 170 | * {@inheritDoc} **Example:** 171 | * ```php 172 | * $mikro[DB\CONNECTION] = new PDO('...'); 173 | * ``` 174 | */ 175 | const CONNECTION = 'DB\CONNECTION'; 176 | } 177 | -------------------------------------------------------------------------------- /src/Error.php: -------------------------------------------------------------------------------- 1 | getMessage(), 43 | $exception->getTrace() 44 | ); 45 | } 46 | 47 | if (isset($mikro[EXCEPTIONS][$class])) { 48 | $specific = $mikro[EXCEPTIONS][$class]($exception); 49 | 50 | if ($specific) { 51 | return $specific; 52 | } 53 | } 54 | 55 | if ($callback) { 56 | $callback = \call_user_func_array($callback, [$exception]); 57 | 58 | if ($callback) { 59 | return $callback; 60 | } 61 | } 62 | 63 | return response($exception); 64 | }); 65 | } 66 | 67 | /** 68 | * Prepare exception response 69 | * 70 | * {@inheritDoc} **Example:** 71 | * ```php 72 | * Error\handler(function (\Throwable $e) { 73 | * // handler 74 | * 75 | * Error\response($e); 76 | * }); 77 | * ``` 78 | */ 79 | function response(\Throwable $exception): ?int 80 | { 81 | $class = \get_class($exception); 82 | 83 | if (\PHP_SAPI === 'cli') { 84 | Console\error("{$class} with message '{$exception->getMessage()}'"); 85 | Console\write("in {$exception->getFile()} line {$exception->getLine()}"); 86 | Console\write(\str_repeat('-', \strlen($class))); 87 | Console\write($exception->getTraceAsString()); 88 | 89 | return null; 90 | } 91 | 92 | if (! \error_reporting()) { 93 | return null; 94 | } 95 | 96 | if (Request\wants_json()) { 97 | return Response\json([ 98 | 'message' => $exception->getMessage(), 99 | 'data' => [ 100 | 'exception' => $class, 101 | 'message' => $exception->getMessage(), 102 | 'file' => $exception->getFile(), 103 | 'line' => $exception->getLine(), 104 | 'trace' => $exception->getTrace() 105 | ] 106 | ], 500); 107 | } 108 | 109 | return Response\html('' . Helper\html('html', [ 110 | Helper\html('head', [ 111 | Helper\html('title', $class), 112 | Helper\html('style', ' 113 | html { font: .9em/1.5 sans-serif } 114 | div.exception-wrapper { width: 50%; margin: 0 auto } 115 | h1.exception-title { color: gray; margin: 0 } 116 | h2.exception-message { margin: 0 } 117 | h3.exception-file { color: gray; margin: 0 } 118 | ') 119 | ]), 120 | Helper\html('body', Helper\html('div', [ 121 | Helper\html('h1', $class)->class('exception-title'), 122 | Helper\html('h2', \htmlentities($exception->getMessage()))->class('exception-message'), 123 | Helper\html('h3', "in {$exception->getFile()} line {$exception->getLine()}")->class('exception-file'), 124 | Helper\html('pre', $exception->getTraceAsString())->class('exception-trace') 125 | ])->class('exception-wrapper')) 126 | ]), 500); 127 | } 128 | 129 | /** 130 | * Show all errors and exceptions 131 | * 132 | * {@inheritDoc} **Example:** 133 | * ```php 134 | * Error\show(); 135 | * ``` 136 | */ 137 | function show(): void 138 | { 139 | \ini_set('display_errors', '1'); 140 | \ini_set('display_startup_errors', '1'); 141 | \error_reporting(\E_ALL); 142 | } 143 | 144 | /** 145 | * Hide all errors and exceptions 146 | * 147 | * {@inheritDoc} **Example:** 148 | * ```php 149 | * Error\hide(); 150 | * ``` 151 | */ 152 | function hide(): void 153 | { 154 | \ini_set('display_errors', '0'); 155 | \error_reporting(0); 156 | } 157 | 158 | /** 159 | * Handle spesific exception 160 | * 161 | * {@inheritDoc} **Example:** 162 | * ```php 163 | * Error\handle(\InvalidArgumentException::class, function (\Throwable $e) { 164 | * return Response\html($e->getMessage()); 165 | * }); 166 | * 167 | * throw new \InvalidArgumentException('Invalid argument'); 168 | * ``` 169 | */ 170 | function handle(string $class, callable $callback): void 171 | { 172 | global $mikro; 173 | 174 | $mikro[EXCEPTIONS][$class] = $callback; 175 | } 176 | 177 | /** 178 | * Do not report spesific exception 179 | * 180 | * {@inheritDoc} **Example:** 181 | * ```php 182 | * Error\dont_report(Mikro\Exceptions\MikroException::class); 183 | * ``` 184 | */ 185 | function dont_report(string $class): void 186 | { 187 | global $mikro; 188 | 189 | $mikro[DONT_REPORT][] = $class; 190 | } 191 | 192 | /** 193 | * Log spesific exception with specific log level 194 | * 195 | * {@inheritDoc} **Example:** 196 | * ```php 197 | * Error\log(PDOException::class, Logger\LEVEL_CRITICAL); 198 | * ``` 199 | */ 200 | function log(string $exception = '*', string $logLevel = Logger\LEVEL_DEBUG): void 201 | { 202 | global $mikro; 203 | 204 | $mikro[LOG][$exception] = $logLevel; 205 | } 206 | 207 | /** 208 | * Handled exceptions collection constant 209 | * 210 | * @internal 211 | */ 212 | const EXCEPTIONS = 'Error\EXCEPTIONS'; 213 | 214 | /** 215 | * Collection of hidden exceptions 216 | * 217 | * @internal 218 | */ 219 | const DONT_REPORT = 'Error\DONT_REPORT'; 220 | 221 | /** 222 | * Collection of exception logger 223 | * 224 | * @internal 225 | */ 226 | const LOG = 'Error\LOG'; 227 | } 228 | -------------------------------------------------------------------------------- /src/Event.php: -------------------------------------------------------------------------------- 1 | Logger\debug('Order created', $order)); 15 | * Event\listen('order.created', function ($order) { 16 | * if ($order->status === 'foo') { 17 | * // 18 | * } 19 | * }); 20 | * ``` 21 | */ 22 | function listen(string $name, callable $callback): void 23 | { 24 | global $mikro; 25 | 26 | $mikro[COLLECTION][$name][] = $callback; 27 | } 28 | 29 | /** 30 | * Emits events 31 | * 32 | * {@inheritDoc} **Example:** 33 | * ```php 34 | * Event\emit('order.created', [$order]); 35 | * ``` 36 | */ 37 | function emit(string $name, array $arguments = []): void 38 | { 39 | global $mikro; 40 | 41 | if (! \array_key_exists($name, $mikro[COLLECTION] ?? [])) { 42 | throw new EventException("'{$name}' named event not exists"); 43 | } 44 | 45 | foreach ($mikro[COLLECTION][$name] as $event) { 46 | if (\is_callable($event)) { 47 | \call_user_func_array($event, $arguments); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * Sync listeners with files. Files must be returns callable/Closure 54 | * 55 | * {@inheritDoc} **Example:** 56 | * ```php 57 | * Event\sync(__DIR__ . '/app/events'); 58 | * ``` 59 | */ 60 | function sync(string $path): void 61 | { 62 | foreach (\glob("{$path}/*.php") as $file) { 63 | if (\is_callable($event = require_once($file))) { 64 | listen(\basename($file, '.php'), $event); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Event collection constant 71 | * 72 | * @internal 73 | */ 74 | const COLLECTION = 'Event\COLLECTION'; 75 | } 76 | -------------------------------------------------------------------------------- /src/Exceptions/ContainerException.php: -------------------------------------------------------------------------------- 1 | getStatus() >= 400 && $this->getStatus() < 500; 39 | } 40 | 41 | public function isServerError(): bool 42 | { 43 | return $this->getStatus() >= 500; 44 | } 45 | 46 | public function isCurlError(): bool 47 | { 48 | return ! $this->getStatus(); 49 | } 50 | 51 | public function getResponse(): ?string 52 | { 53 | return self::$response; 54 | } 55 | 56 | public function getStatus(): ?int 57 | { 58 | return self::$status; 59 | } 60 | 61 | public function getDetails(): array 62 | { 63 | return self::$details; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Exceptions/DataNotFoundException.php: -------------------------------------------------------------------------------- 1 | generateRandom(); 19 | * ``` 20 | */ 21 | public static function generateRandom(int $strength): string 22 | { 23 | if (\extension_loaded('openssl')) { 24 | return \hash('sha512', \openssl_random_pseudo_bytes($strength)); 25 | } 26 | 27 | return \hash('sha512', \random_bytes($strength)); 28 | } 29 | 30 | /** 31 | * Validates CSRF token 32 | * 33 | * {@inheritDoc} **Example:** 34 | * ```php 35 | * if (! Helper\csrf()->validate(Request\get('__CSRF_TOKEN'))) { 36 | * throw new Exception('CSRF token does not match'); 37 | * } 38 | * ``` 39 | */ 40 | public static function validate(?string $value = null): bool 41 | { 42 | if (! isset($_SESSION['__csrf'])) { 43 | return false; 44 | } 45 | 46 | if ($value === null) { 47 | $value = Request\get('__CSRF_TOKEN'); 48 | } 49 | 50 | return \hash_equals($value, $_SESSION['__csrf']); 51 | } 52 | 53 | /** 54 | * Generate CSRF token 55 | * 56 | * {@inheritDoc} **Example:** 57 | * ```php 58 | * $token = Helper\csrf()->get(); 59 | * ``` 60 | */ 61 | public static function get(): string 62 | { 63 | if (\session_status() !== \PHP_SESSION_ACTIVE) { 64 | throw new MikroException('Start the PHP Session first'); 65 | } 66 | 67 | if (isset($_SESSION['__csrf'])) { 68 | return $_SESSION['__csrf']; 69 | } 70 | 71 | return $_SESSION['__csrf'] = self::generateRandom(32); 72 | } 73 | 74 | /** 75 | * Generate CSRF input in HTML 76 | * 77 | * {@inheritDoc} **Example:** 78 | * ```php 79 | * echo Helper\csrf()->field(); 80 | * ``` 81 | */ 82 | public static function field(): string 83 | { 84 | return (string) html('input', '') 85 | ->name('__CSRF_TOKEN') 86 | ->type('hidden') 87 | ->value(self::get()); 88 | } 89 | }; 90 | } 91 | 92 | function flash(?string $message = null): mixed 93 | { 94 | $flash = new class { 95 | /** 96 | * Add flash message to session store 97 | * 98 | * {@inheritDoc} **Example:** 99 | * ```php 100 | * Helper\flash()->add('Flash message'); // add default message 101 | * Helper\flash()->add('Flash message', 'error'); // add error message 102 | * ``` 103 | */ 104 | public static function add(string|array $message, string $type = 'default'): string|array 105 | { 106 | if (\session_status() !== \PHP_SESSION_ACTIVE) { 107 | throw new MikroException('Start the PHP Session first'); 108 | } 109 | 110 | $messages = $_SESSION['__flash_' . $type] ?? []; 111 | 112 | if (is_array($message)) { 113 | $messages = array_merge($messages, $message); 114 | } else { 115 | $messages[] = $message; 116 | } 117 | 118 | return $_SESSION['__flash_' . $type] = $messages; 119 | } 120 | 121 | /** 122 | * Get flash messages on session store 123 | * 124 | * {@inheritDoc} **Example:** 125 | * ```php 126 | * Helper\flash()->get(); // get default messages 127 | * Helper\flash()->set('error'); // get error messages 128 | * ``` 129 | */ 130 | public static function get(string $type = 'default'): ?array 131 | { 132 | if (\session_status() !== \PHP_SESSION_ACTIVE) { 133 | throw new MikroException('Start the PHP Session first'); 134 | } 135 | 136 | $messages = $_SESSION['__flash_' . $type] ?? []; 137 | unset($_SESSION['__flash_' . $type]); 138 | 139 | return $messages; 140 | } 141 | }; 142 | 143 | return $message ? $flash->add($message) : $flash; 144 | } 145 | 146 | /** 147 | * Creates a Html tag 148 | * 149 | * {@inheritDoc} **Example:** 150 | * ```php 151 | * echo Helper\html('br'); //
152 | * echo Helper\html('table') //
153 | * echo Helper\html('p', 'Hello world!'); //

Hello world!

154 | * echo Helper\html('p', 'Hey!', ['class' => 'hey']); //

Hey!

155 | * echo Helper\html('p', 'Hey!')->class('hey'); //

Hey!

156 | * echo Helper\html('p', 'Hey!', ['PascalCase' => 'true'])->snakeCase('true')->kebabCase('false'); 157 | * //

Hey!

158 | * 159 | * echo Helper\html('div', [ 160 | * Helper\html('a', 'Link')->href('http://url.com')->style('text-decoration:none;'), 161 | * Helper\html('a', 'Link')->href('http://url.com')->style(['text-decoration' => 'none']) 162 | * ]); // same result 163 | * ``` 164 | * 165 | * New feature: Invokable tag 166 | * Always returns string, instead of \Stringable 167 | * ```php 168 | * Helper\html('div')->class('foo')('Content'); //
content
169 | * Helper\html('span')('Text', ['class' => 'bg-light']); // Text 170 | * Helper\html('ul', Helper\html('li')('Item'))(); // 171 | * ``` 172 | */ 173 | function html(string $name, mixed $content = '', array $attributes = []): object 174 | { 175 | return new class ($name, $content, $attributes) implements \Stringable { 176 | public function __construct( 177 | protected string $name, 178 | protected mixed $content, 179 | protected array $attributes = [] 180 | ) { 181 | if (\is_array($content)) { 182 | $content = \implode('', \array_map(fn($tag) => (string) $tag, $content)); 183 | } 184 | 185 | $this->content = (string) $content; 186 | } 187 | 188 | public function __call(string $name, array $args): self 189 | { 190 | $name = \strtolower(\preg_replace('/(?attributes[$name] = isset($this->attributes[$name]) && $append === true ? 195 | $this->attributes[$name] . $attribute : $attribute; 196 | 197 | return $this; 198 | } 199 | 200 | public function __toString(): string 201 | { 202 | $attributes = ''; 203 | 204 | foreach ($this->attributes as $key => $value) { 205 | if (\is_array($value)) { 206 | $value = \implode('', \array_map(function ($key, $value) { 207 | return "{$key}:{$value};"; 208 | }, \array_keys($value), \array_values($value))); 209 | } 210 | 211 | if ($value === null) { 212 | $attributes .= "{$key} "; 213 | } elseif ($value === false) { 214 | // pass 215 | } else { 216 | $attributes .= \sprintf('%s="%s" ', $key, $value); 217 | } 218 | } 219 | 220 | $attributes = empty($attributes) ? '' : ' ' . \trim($attributes); 221 | $tag = \sprintf('<%s%s>', $this->name, $attributes); 222 | 223 | if ($this->content === "0" || $this->content) { 224 | $tag .= $this->content; 225 | } 226 | 227 | $selfCloseTags = [ 228 | 'br', 229 | 'hr', 230 | 'col', 231 | 'img', 232 | 'wbr', 233 | 'area', 234 | 'base', 235 | 'link', 236 | 'meta', 237 | 'embed', 238 | 'input', 239 | 'param', 240 | 'track', 241 | 'source', 242 | ]; 243 | 244 | if (! \in_array($this->name, $selfCloseTags)) { 245 | $tag .= \sprintf('', $this->name); 246 | } 247 | 248 | return $tag; 249 | } 250 | 251 | public function __set(string $attribute, mixed $value): void 252 | { 253 | $this->attributes[$attribute] = $value; 254 | } 255 | 256 | public function __get(string $attribute): mixed 257 | { 258 | return $this->attributes[$attribute] ?? null; 259 | } 260 | 261 | public function __isset(string $attribute): bool 262 | { 263 | return \array_key_exists($attribute, $this->attributes); 264 | } 265 | }; 266 | } 267 | 268 | /** 269 | * Creates a Curl request 270 | * 271 | * {@inheritDoc} **Example:** 272 | * ```php 273 | * $curl = Helper\curl('http://url.com'); 274 | * $textResponse = (string) $curl->exec(); // string 275 | * $textResponse = $curl->text(); // string 276 | * $arrayResponse = $curl->json(); // array or null 277 | * 278 | * // Request with data 279 | * Helper\curl('http://foo.com')->data(['foo' => 'bar'])->json(); // send request with json body 280 | * Helper\curl('http://foo.com')->asForm()->data(['foo' => 'bar'])->json(); // send request as form 281 | * Helper\curl('http://foo.com')->asForm()->data(['foo' => 'bar'])->json(); // send request as form 282 | * 283 | * // Request with header 284 | * Helper\curl('http://foo.com')->headers(['Content-type' => 'application/json'])->json(); 285 | * 286 | * // Request with another method 287 | * Helper\curl('http://foo.com', 'post'); 288 | * Helper\curl('http://foo.com')->method('post')->json(); 289 | * 290 | * // Get request details 291 | * $curl = Helper\curl('http://foo.com')->method('post')->data(['x' => 'y'])->exec(); 292 | * 293 | * $curl->getInfo(); // info array 294 | * https://www.php.net/manual/function.curl-getinfo.php 295 | * $curl->getInfo('http_code'); // 200 296 | * $curl->getInfo('url'); // http://foo.com 297 | * $curl->getInfo('total_time'); 298 | * $curl->getInfo('connect_time'); 299 | * $curl->getInfo('redirect_url'); 300 | * $curl->getInfo('request_header'); 301 | * ... 302 | * 303 | * // Request with Curl options 304 | * Helper\curl('http://foo.com', 'PUT', [ 305 | * \CURLOPT_FOLLOWLOCATION => true 306 | * ]); 307 | * Helper\curl('http://foo.com', 'PUT')->followLocation(); // same as above 308 | * 309 | * Helper\curl('http://foo.com')->options([ 310 | * \CURLOPT_FOLLOWLOCATION => true, 311 | * \CURLOPT_URL => 'http://newurl.com' 312 | * ])->json(); 313 | * ``` 314 | */ 315 | function curl(string $url, string $method = 'GET', array $options = []): object 316 | { 317 | return new class ($url, $method, $options) { 318 | /** 319 | * Curl resource 320 | * 321 | * @var mixed 322 | */ 323 | public mixed $curl; 324 | 325 | /** 326 | * Send request as form 327 | * 328 | * @var boolean 329 | */ 330 | public bool $asForm = false; 331 | 332 | /** 333 | * Throw exception on fail 334 | * 335 | * @var boolean 336 | */ 337 | public bool $errorOnFail = false; 338 | 339 | /** 340 | * Response data 341 | * 342 | * @var null|string 343 | */ 344 | public ?string $response = null; 345 | 346 | public function __construct( 347 | public string $url, 348 | public string $method, 349 | public array $options 350 | ) { 351 | $this->method($method); 352 | $this->options($options); 353 | $this->curl = \curl_init(); 354 | } 355 | 356 | /** 357 | * Execute actual request 358 | * 359 | * {@inheritDoc} **Example:** 360 | * ```php 361 | * Helper\curl('url')->exec(); 362 | * ``` 363 | */ 364 | public function exec(): self 365 | { 366 | $options = [ 367 | \CURLOPT_URL => $this->url, 368 | \CURLOPT_RETURNTRANSFER => true 369 | ]; 370 | 371 | if ($this->method !== 'GET' && ! isset($this->options[\CURLOPT_POSTFIELDS])) { 372 | $options[\CURLOPT_POSTFIELDS] = ''; 373 | } 374 | 375 | if (\in_array($this->method, ['HEAD', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'])) { 376 | $options[\CURLOPT_CUSTOMREQUEST] = $this->method; 377 | } 378 | 379 | \curl_setopt_array($this->curl, $options + $this->options); 380 | 381 | $this->response = (string) \curl_exec($this->curl); 382 | 383 | if ($this->errorOnFail) { 384 | if (! empty($this->getError())) { 385 | throw CurlException::curlError($this->getError(), [ 386 | 'method' => $this->method, 387 | 'options' => $options + $this->options, 388 | 'info' => $this->getInfo(), 389 | ]); 390 | } 391 | 392 | if ($this->isClientError()) { 393 | throw CurlException::clientError($this->text(), $this->getStatus(), [ 394 | 'method' => $this->method, 395 | 'options' => $options + $this->options, 396 | 'info' => $this->getInfo(), 397 | ]); 398 | } 399 | 400 | if ($this->isServerError()) { 401 | throw CurlException::clientError($this->text(), $this->getStatus(), [ 402 | 'method' => $this->method, 403 | 'options' => $options + $this->options, 404 | 'info' => $this->getInfo(), 405 | ]); 406 | } 407 | } 408 | 409 | return $this; 410 | } 411 | 412 | public function __toString(): string 413 | { 414 | return $this->response ?? ''; 415 | } 416 | 417 | /** 418 | * Return response as text 419 | * 420 | * {@inheritDoc} **Example:** 421 | * ```php 422 | * Helper\curl('url')->text(); 423 | * ``` 424 | */ 425 | public function text(): string 426 | { 427 | if ($this->response === null) { 428 | $this->exec(); 429 | } 430 | 431 | return (string) $this->response; 432 | } 433 | 434 | /** 435 | * Return response as array 436 | * 437 | * {@inheritDoc} **Example:** 438 | * ```php 439 | * Helper\curl('url')->json(); 440 | * ``` 441 | */ 442 | public function json(): ?array 443 | { 444 | if ($this->response === null) { 445 | $this->exec(); 446 | } 447 | 448 | try { 449 | return \json_decode($this->response, true, 512, \JSON_THROW_ON_ERROR); 450 | } catch (\JsonException) { 451 | return null; 452 | } 453 | } 454 | 455 | /** 456 | * Return curl info data 457 | * 458 | * {@inheritDoc} **Example:** 459 | * ```php 460 | * $curl = Helper\curl('url')->exec(); 461 | * $status = $curl->getInfo('status_code'); 462 | * ``` 463 | */ 464 | public function getInfo(?string $key = null): mixed 465 | { 466 | if (\curl_errno($this->curl)) { 467 | return null; 468 | } 469 | 470 | $info = \curl_getinfo($this->curl); 471 | 472 | if ($key === null) { 473 | return $info; 474 | } 475 | 476 | return $info[$key] ?? null; 477 | } 478 | 479 | /** 480 | * Set request method 481 | * 482 | * {@inheritDoc} **Example:** 483 | * ```php 484 | * Helper\curl('url')->method('POST')->exec(); 485 | * ``` 486 | */ 487 | public function method(string $method): self 488 | { 489 | $this->method = \strtoupper($method); 490 | 491 | return $this; 492 | } 493 | 494 | /** 495 | * Set curl options 496 | * 497 | * {@inheritDoc} **Example:** 498 | * ```php 499 | * Helper\curl('url')->options(['CURLOPT_FOLLOWLOCATION' => true])->exec(); 500 | * ``` 501 | */ 502 | public function options(array $options): self 503 | { 504 | $this->options = $options; 505 | 506 | return $this; 507 | } 508 | 509 | /** 510 | * Set request form/body data 511 | * 512 | * {@inheritDoc} **Example:** 513 | * ```php 514 | * Helper\curl('url')->data(['foo' => 'bar'])->exec(); 515 | * ``` 516 | */ 517 | public function data(array $data): self 518 | { 519 | $data = $this->asForm ? \http_build_query($data) : \json_encode($data); 520 | 521 | switch ($this->method) { 522 | case 'GET': 523 | $this->url .= '?' . $data; 524 | break; 525 | 526 | default: 527 | $this->options[\CURLOPT_POSTFIELDS] = $data; 528 | break; 529 | } 530 | 531 | return $this; 532 | } 533 | 534 | /** 535 | * Set request headers 536 | * 537 | * {@inheritDoc} **Example:** 538 | * ```php 539 | * Helper\curl('url')->headers(['Authorization' => 'Bearer token'])->exec(); 540 | * ``` 541 | */ 542 | public function headers(array $headers): self 543 | { 544 | $this->options[\CURLOPT_HTTPHEADER] = \array_map( 545 | fn($key, $val) => "{$key}: {$val}", 546 | \array_keys($headers), 547 | $headers 548 | ); 549 | 550 | return $this; 551 | } 552 | 553 | /** 554 | * Set curl 'follow location' parameter 555 | * 556 | * {@inheritDoc} **Example:** 557 | * ```php 558 | * Helper\curl('url')->followLocation()->exec(); 559 | * ``` 560 | */ 561 | public function followLocation(): self 562 | { 563 | $this->options[\CURLOPT_FOLLOWLOCATION] = true; 564 | 565 | return $this; 566 | } 567 | 568 | /** 569 | * Send request as form 570 | * 571 | * {@inheritDoc} **Example:** 572 | * ```php 573 | * Helper\curl('url')->asForm()->data(['foo' => 'bar'])->exec(); 574 | * ``` 575 | */ 576 | public function asForm(): self 577 | { 578 | $this->asForm = true; 579 | 580 | return $this; 581 | } 582 | 583 | /** 584 | * Get actual response status 585 | * 586 | * {@inheritDoc} **Example:** 587 | * ```php 588 | * Helper\curl('url')->exec()->getStatus(); // 200 589 | * ``` 590 | */ 591 | public function getStatus(): ?int 592 | { 593 | return $this->getInfo('http_code'); 594 | } 595 | 596 | /** 597 | * Get curl error 598 | * 599 | * {@inheritDoc} **Example:** 600 | * ```php 601 | * Helper\curl('url')->exec()->getError(); 602 | * ``` 603 | */ 604 | public function getError(): string 605 | { 606 | return \curl_error($this->curl); 607 | } 608 | 609 | /** 610 | * Check is request successful 611 | * 612 | * {@inheritDoc} **Example:** 613 | * ```php 614 | * $request = Helper\curl('url')->exec(); 615 | * 616 | * $request->isOk(); // bool 617 | * ``` 618 | */ 619 | public function isOk(): bool 620 | { 621 | return $this->getStatus() >= 200 && $this->getStatus() < 300; 622 | } 623 | 624 | /** 625 | * Check is request redirected 626 | * 627 | * {@inheritDoc} **Example:** 628 | * ```php 629 | * $request = Helper\curl('url')->exec(); 630 | * 631 | * $request->isRedirect(); 632 | * ``` 633 | */ 634 | public function isRedirect(): bool 635 | { 636 | return $this->getStatus() >= 300 && $this->getStatus() < 400; 637 | } 638 | 639 | /** 640 | * Check is request failed 641 | * 642 | * {@inheritDoc} **Example:** 643 | * ```php 644 | * $request = Helper\curl('url')->exec(); 645 | * 646 | * $request->isFailed(); 647 | * ``` 648 | */ 649 | public function isFailed(): bool 650 | { 651 | return $this->isServerError() || $this->isClientError(); 652 | } 653 | 654 | /** 655 | * Check is server responded 5xx request 656 | * 657 | * {@inheritDoc} **Example:** 658 | * ```php 659 | * $request = Helper\curl('url')->exec(); 660 | * 661 | * $request->isServerError(); 662 | * ``` 663 | */ 664 | public function isServerError(): bool 665 | { 666 | return $this->getStatus() >= 500; 667 | } 668 | 669 | /** 670 | * Check is server responded 4xx request 671 | * 672 | * {@inheritDoc} **Example:** 673 | * ```php 674 | * $request = Helper\curl('url')->exec(); 675 | * 676 | * $request->isClientError(); 677 | * ``` 678 | */ 679 | public function isClientError(): bool 680 | { 681 | return $this->getStatus() >= 400 && $this->getStatus() < 500; 682 | } 683 | 684 | /** 685 | * Set request is failed, throw an exception 686 | * 687 | * {@inheritDoc} **Example:** 688 | * ```php 689 | * Helper\curl('url')->errorOnFail()->exec(); 690 | * ``` 691 | */ 692 | public function errorOnFail(bool $errorOnFail = true): self 693 | { 694 | $this->errorOnFail = $errorOnFail; 695 | 696 | return $this; 697 | } 698 | }; 699 | } 700 | 701 | /** 702 | * Returns pagination data based on the total number of items 703 | * 704 | * {@inheritDoc} **Example:** 705 | * ```php 706 | * Helper\paginate(100); 707 | * Helper\paginate(100, 2); 708 | * Helper\paginate(100, Request\input('page'), 25); 709 | * ``` 710 | */ 711 | function paginate(int|string $total, int|string $page = 1, int|string $limit = 10): object 712 | { 713 | return new class ( 714 | \intval($total), 715 | \intval($page), 716 | \intval($limit) 717 | ) implements \ArrayAccess, \IteratorAggregate, \Countable { 718 | public array $data; 719 | public array $items = []; 720 | 721 | public function __construct(public int $total, public int $page, public int $limit) 722 | { 723 | $this->data = [ 724 | 'page' => $page = $page < 1 ? 1 : $page, 725 | 'max' => $max = \ceil($total / $limit) * $limit, 726 | 'limit' => $limit, 727 | 'offset' => ($offset = ($page - 1) * $limit) > $max ? $max : $offset, 728 | 'total_page' => $totalPage = \intval($max / $limit), 729 | 'current_page' => $currentPage = $page > $totalPage ? $totalPage : $page, 730 | 'next_page' => $currentPage + 1 > $totalPage ? $totalPage : $currentPage + 1, 731 | 'previous_page' => $currentPage - 1 ?: 1, 732 | ]; 733 | } 734 | 735 | /** 736 | * Get pagination data 737 | * 738 | * {@inheritDoc} **Example:** 739 | * ```php 740 | * Helper\paginate(100, Request\get('currentPage'))->getData(); 741 | * ``` 742 | */ 743 | public function getData(): array 744 | { 745 | return $this->data; 746 | } 747 | 748 | /** 749 | * Get current page 750 | * 751 | * {@inheritDoc} **Example:** 752 | * ```php 753 | * Helper\paginate(100, Request\get('currentPage'))->getPage(); 754 | * ``` 755 | */ 756 | public function getPage(): int 757 | { 758 | return $this->data['page']; 759 | } 760 | 761 | /** 762 | * Get pagination limit 763 | * 764 | * {@inheritDoc} **Example:** 765 | * ```php 766 | * Helper\paginate(100, Request\get('currentPage'))->getLimit(); 767 | * ``` 768 | */ 769 | public function getLimit(): int 770 | { 771 | return $this->data['limit']; 772 | } 773 | 774 | /** 775 | * Get pagination offset 776 | * 777 | * {@inheritDoc} **Example:** 778 | * ```php 779 | * Helper\paginate(100, Request\get('currentPage'))->getOffset(); 780 | * ``` 781 | */ 782 | public function getOffset(): int 783 | { 784 | return $this->data['offset']; 785 | } 786 | 787 | /** 788 | * Get pagination total page 789 | * 790 | * {@inheritDoc} **Example:** 791 | * ```php 792 | * $totalPage = Helper\paginate(100, Request\get('currentPage'))->getTotalPage(); 793 | * $pages = range(1, $totalPage); 794 | * ``` 795 | */ 796 | public function getTotalPage(): int 797 | { 798 | return $this->data['total_page']; 799 | } 800 | 801 | /** 802 | * Get pagination current page 803 | * 804 | * {@inheritDoc} **Example:** 805 | * ```php 806 | * Helper\paginate(100, Request\get('currentPage'))->getCurrentPage(); 807 | * ``` 808 | */ 809 | public function getCurrentPage(): int 810 | { 811 | return $this->data['current_page']; 812 | } 813 | 814 | /** 815 | * Get pagination next page 816 | * 817 | * {@inheritDoc} **Example:** 818 | * ```php 819 | * Helper\paginate(100, Request\get('currentPage'))->getNextPage(); 820 | * ``` 821 | */ 822 | public function getNextPage(): int 823 | { 824 | return $this->data['next_page']; 825 | } 826 | 827 | /** 828 | * Get pagination previous page 829 | * 830 | * {@inheritDoc} **Example:** 831 | * ```php 832 | * Helper\paginate(100, Request\get('currentPage'))->getPreviousPage(); 833 | * ``` 834 | */ 835 | public function getPreviousPage(): int 836 | { 837 | return $this->data['previous_page']; 838 | } 839 | 840 | /** 841 | * Get pagination pages data as array 842 | * 843 | * {@inheritDoc} **Example:** 844 | * ```php 845 | * Helper\paginate(100, Request\get('currentPage'))->getPages(); 846 | * ``` 847 | */ 848 | public function getPageNumbers(): array 849 | { 850 | return \range(1, $this->getTotalPage()); 851 | } 852 | 853 | /** 854 | * Set pagination items 855 | * 856 | * {@inheritDoc} **Example:** 857 | * ```php 858 | * $paginator = Helper\paginate(100, Request\get('currentPage')); 859 | * $paginator->setItems($data); 860 | * ``` 861 | */ 862 | public function setItems(array $items): self 863 | { 864 | $this->items = $items; 865 | 866 | return $this; 867 | } 868 | 869 | /** 870 | * Get pagination items 871 | * 872 | * {@inheritDoc} **Example:** 873 | * ```php 874 | * $paginator = Helper\paginate(100, Request\get('currentPage')); 875 | * $paginator->setItems($data); 876 | * $items = $paginator->getItems(); 877 | * ``` 878 | */ 879 | public function getItems(): array 880 | { 881 | return $this->items; 882 | } 883 | 884 | public function getIterator(): \Traversable 885 | { 886 | return new \ArrayIterator($this->getItems()); 887 | } 888 | 889 | public function count(): int 890 | { 891 | return \count($this->getItems()); 892 | } 893 | 894 | public function offsetExists(mixed $offset): bool 895 | { 896 | return isset($this->items[$offset]); 897 | } 898 | 899 | public function offsetGet(mixed $offset): mixed 900 | { 901 | return $this->items[$offset]; 902 | } 903 | 904 | public function offsetSet(mixed $offset, mixed $value): void 905 | { 906 | if ($offset === null) { 907 | $this->items[] = $value; 908 | } else { 909 | $this->items[$offset] = $value; 910 | } 911 | } 912 | 913 | public function offsetUnset(mixed $offset): void 914 | { 915 | unset($this->items[$offset]); 916 | } 917 | }; 918 | } 919 | } 920 | -------------------------------------------------------------------------------- /src/Jwt.php: -------------------------------------------------------------------------------- 1 | $uid, 47 | 'exp' => $expiration, 48 | 'iss' => $issuer, 49 | 'iat' => $iat ?? \time() 50 | ]); 51 | } 52 | 53 | /** 54 | * Decode Jwt token 55 | * 56 | * {@inheritDoc} **Example:** 57 | * ```php 58 | * try { 59 | * $object = Jwt\decode('Jwt token'); 60 | * $uid = $object->uid; 61 | * } catch (Exception $e) { 62 | * echo 'Jwt token invalid'; 63 | * } 64 | * 65 | * $object = Jwt\decode('Jwt token', false); 66 | * ``` 67 | * 68 | * @throws JwtException If invalid Jwt token string 69 | * @throws JwtException If invalid segments encoding 70 | * @throws JwtException If empty algorithm 71 | * @throws JwtException If signature verification fail 72 | */ 73 | function decode(string $jwt, bool $verify = true): object 74 | { 75 | $pieces = \explode('.', $jwt); 76 | 77 | if (\count($pieces) !== 3) { 78 | throw new JwtException('Wrong number of segments'); 79 | } 80 | 81 | [$headerB64, $payloadB64, $cryptoB64] = $pieces; 82 | 83 | $header = \json_decode(url_safe_base64_decode($headerB64), false, 512, \JSON_THROW_ON_ERROR); 84 | $payload = \json_decode(url_safe_base64_decode($payloadB64), false, 512, \JSON_THROW_ON_ERROR); 85 | 86 | if (empty(\get_object_vars($header))) { 87 | throw new JwtException('Invalid segment encoding'); 88 | } 89 | 90 | if (empty(\get_object_vars($payload))) { 91 | throw new JwtException('Invalid segment encoding'); 92 | } 93 | 94 | $sign = url_safe_base64_decode($cryptoB64); 95 | 96 | if ($verify) { 97 | if (! isset($header->alg) || empty($header->alg)) { 98 | throw new JwtException('Empty algorithm'); 99 | } 100 | 101 | if ($sign != sign("$headerB64.$payloadB64", $header->alg)) { 102 | throw new JwtException('Signature verification failed'); 103 | } 104 | } 105 | 106 | return $payload; 107 | } 108 | 109 | /** 110 | * Creates Jwt token with object/array 111 | * 112 | * {@inheritDoc} **Example:** 113 | * ```php 114 | * Jwt\encode(['uid' => 1, 'iat' => time(), 'exp' => time() + 60, 'iss' => 'foo']); 115 | * ``` 116 | */ 117 | function encode(object|array $payload, string $algo = 'HS256'): string 118 | { 119 | $header = ['typ' => 'JWT', 'alg' => $algo]; 120 | $segments = []; 121 | $segments[] = url_safe_base64_encode(\json_encode($header, \JSON_THROW_ON_ERROR)); 122 | $segments[] = url_safe_base64_encode(\json_encode($payload, \JSON_THROW_ON_ERROR)); 123 | $input = \implode('.', $segments); 124 | $signature = sign($input, $algo); 125 | $segments[] = url_safe_base64_encode($signature); 126 | 127 | return \implode('.', $segments); 128 | } 129 | 130 | /** 131 | * Signs Jwt payload 132 | * 133 | * @internal 134 | * @throws JwtException If algorithm not supported 135 | */ 136 | function sign(string $message, string $method = 'HS256'): string 137 | { 138 | $methods = [ 139 | 'HS256' => 'sha256', 140 | 'HS384' => 'sha384', 141 | 'HS512' => 'sha512', 142 | ]; 143 | 144 | if (! isset($methods[$method])) { 145 | throw new JwtException('Algorithm not supported'); 146 | } 147 | 148 | return \hash_hmac($methods[$method], $message, secret(), true); 149 | } 150 | 151 | /** 152 | * @internal 153 | */ 154 | function url_safe_base64_decode(string $input): string 155 | { 156 | $remainder = \strlen($input) % 4; 157 | 158 | if ($remainder) { 159 | $padlen = 4 - $remainder; 160 | $input .= \str_repeat('=', $padlen); 161 | } 162 | 163 | return \base64_decode(\strtr($input, '-_', '+/')); 164 | } 165 | 166 | /** 167 | * @internal 168 | */ 169 | function url_safe_base64_encode(string $input): string 170 | { 171 | return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); 172 | } 173 | 174 | /** 175 | * Validates token 176 | * 177 | * {@inheritDoc} **Example:** 178 | * ```php 179 | * if (Jwt\validate('token')) { 180 | * echo 'It\'s ok!'; 181 | * } 182 | * ``` 183 | */ 184 | function validate(string $token): bool 185 | { 186 | try { 187 | decode($token); 188 | 189 | return true; 190 | } catch (JwtException) { 191 | return false; 192 | } 193 | } 194 | 195 | /** 196 | * Check token is expired 197 | * 198 | * {@inheritDoc} **Example:** 199 | * ```php 200 | * if (! Jwt\expired('token')) { 201 | * echo 'It\'s ok!'; 202 | * } 203 | * ``` 204 | */ 205 | function expired(string $token): bool 206 | { 207 | if (! validate($token)) { 208 | return false; 209 | } 210 | 211 | $payload = decode($token); 212 | 213 | if (! \property_exists($payload, 'exp')) { 214 | return false; 215 | } 216 | 217 | return $payload->exp < \time(); 218 | } 219 | 220 | /** 221 | * Jwt secret constant 222 | * 223 | * {@inheritDoc} **Example:** 224 | * ```php 225 | * $mikro[Jwt\SECRET] = 'secretstring'; 226 | * ``` 227 | */ 228 | const SECRET = 'Jwt\SECRET'; 229 | } 230 | -------------------------------------------------------------------------------- /src/Locale.php: -------------------------------------------------------------------------------- 1 | 'Merhaba Dünya' 21 | * ]; 22 | * ``` 23 | */ 24 | function t(string $phrase): string 25 | { 26 | return data()[$phrase] ?? $phrase; 27 | } 28 | 29 | /** 30 | * Sets current locale 31 | * 32 | * {@inheritDoc} **Example:** 33 | * ```php 34 | * Locale\set('tr'); 35 | * ``` 36 | */ 37 | function set(string $locale): void 38 | { 39 | global $mikro; 40 | 41 | $mikro[CURRENT] = $locale; 42 | } 43 | 44 | /** 45 | * Gets current locale 46 | * 47 | * {@inheritDoc} **Example:** 48 | * ```php 49 | * Locale\get(); // tr 50 | * ``` 51 | */ 52 | function get(): string 53 | { 54 | global $mikro; 55 | 56 | return $mikro[CURRENT] ?? ($mikro[FALLBACK] ?? 'en'); 57 | } 58 | 59 | /** 60 | * Gets current localization data 61 | * 62 | * {@inheritDoc} **Example:** 63 | * ```php 64 | * Locale\data(); // array 65 | * ``` 66 | * 67 | * @throw MikroException If not have mikro locale path 68 | */ 69 | function data(): array 70 | { 71 | global $mikro; 72 | 73 | if (! isset($mikro[DATA][get()])) { 74 | if (! isset($mikro[PATH])) { 75 | throw new MikroException('Please specify the locale path'); 76 | } 77 | 78 | $path = $mikro[PATH] . \DIRECTORY_SEPARATOR . get() . '.php'; 79 | $mikro[DATA][get()] = \is_file($path) ? (array) require($path) : []; 80 | } 81 | 82 | return $mikro[DATA][get()]; 83 | } 84 | 85 | /** 86 | * Localization files path constant 87 | * 88 | * {@inheritDoc} **Example:** 89 | * ```php 90 | * $mikro[Locale\PATH] = __DIR__ . '/languages'; 91 | * ``` 92 | */ 93 | const PATH = 'Locale\PATH'; 94 | 95 | /** 96 | * Current language constant 97 | * 98 | * {@inheritDoc} **Example:** 99 | * ```php 100 | * $mikro[Locale\CURRENT] = 'tr'; 101 | * ``` 102 | */ 103 | const CURRENT = 'Locale\CURRENT'; 104 | 105 | /** 106 | * Fallback language constant 107 | * 108 | * {@inheritDoc} **Example:** 109 | * ```php 110 | * $mikro[Locale\FALLBACK] = 'en'; 111 | * ``` 112 | */ 113 | const FALLBACK = 'Locale\FALLBACK'; 114 | 115 | /** 116 | * @internal 117 | */ 118 | const DATA = 'Locale\DATA'; 119 | } 120 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | 'data']); 95 | * ``` 96 | */ 97 | function emergency(string $message, array|object|null $context = null): void 98 | { 99 | log(LEVEL_EMERGENCY, $message, $context); 100 | } 101 | 102 | /** 103 | * Writes to log file with 'alert' level 104 | * 105 | * {@inheritDoc} **Example:** 106 | * ```php 107 | * Logger\alert('log message', ['log' => 'data']); 108 | * ``` 109 | */ 110 | function alert(string $message, array|object|null $context = null): void 111 | { 112 | log(LEVEL_ALERT, $message, $context); 113 | } 114 | 115 | /** 116 | * Writes to log file with 'critical' level 117 | * 118 | * {@inheritDoc} **Example:** 119 | * ```php 120 | * Logger\critical('log message', ['log' => 'data']); 121 | * ``` 122 | */ 123 | function critical(string $message, array|object|null $context = null): void 124 | { 125 | log(LEVEL_CRITICAL, $message, $context); 126 | } 127 | 128 | /** 129 | * Writes to log file with 'error' level 130 | * 131 | * {@inheritDoc} **Example:** 132 | * ```php 133 | * Logger\error('log message', ['log' => 'data']); 134 | * ``` 135 | */ 136 | function error(string $message, array|object|null $context = null): void 137 | { 138 | log(LEVEL_ERROR, $message, $context); 139 | } 140 | 141 | /** 142 | * Writes to log file with 'warning' level 143 | * 144 | * {@inheritDoc} **Example:** 145 | * ```php 146 | * Logger\warning('log message', ['log' => 'data']); 147 | * ``` 148 | */ 149 | function warning(string $message, array|object|null $context = null): void 150 | { 151 | log(LEVEL_WARNING, $message, $context); 152 | } 153 | 154 | /** 155 | * Writes to log file with 'notice' level 156 | * 157 | * {@inheritDoc} **Example:** 158 | * ```php 159 | * Logger\notice('log message', ['log' => 'data']); 160 | * ``` 161 | */ 162 | function notice(string $message, array|object|null $context = null): void 163 | { 164 | log(LEVEL_NOTICE, $message, $context); 165 | } 166 | 167 | /** 168 | * Writes to log file with 'info' level 169 | * 170 | * {@inheritDoc} **Example:** 171 | * ```php 172 | * Logger\info('log message', ['log' => 'data']); 173 | * ``` 174 | */ 175 | function info(string $message, array|object|null $context = null): void 176 | { 177 | log(LEVEL_INFO, $message, $context); 178 | } 179 | 180 | /** 181 | * Writes to log file with 'debug' level 182 | * 183 | * {@inheritDoc} **Example:** 184 | * ```php 185 | * Logger\debug('log message', ['log' => 'data']); 186 | * ``` 187 | */ 188 | function debug(string $message, array|object|null $context = null): void 189 | { 190 | log(LEVEL_DEBUG, $message, $context); 191 | } 192 | 193 | /** 194 | * Constant of emergency level 195 | */ 196 | const LEVEL_EMERGENCY = 'emergency'; 197 | 198 | /** 199 | * Constant of alert level 200 | */ 201 | const LEVEL_ALERT = 'alert'; 202 | 203 | /** 204 | * Constant of critical level 205 | */ 206 | const LEVEL_CRITICAL = 'critical'; 207 | 208 | /** 209 | * Constant of error level 210 | */ 211 | const LEVEL_ERROR = 'error'; 212 | 213 | /** 214 | * Constant of warning level 215 | */ 216 | const LEVEL_WARNING = 'warning'; 217 | 218 | /** 219 | * Constant of notice level 220 | */ 221 | const LEVEL_NOTICE = 'notice'; 222 | 223 | /** 224 | * Constant of info level 225 | */ 226 | const LEVEL_INFO = 'info'; 227 | 228 | /** 229 | * Constant of debug level 230 | */ 231 | const LEVEL_DEBUG = 'debug'; 232 | 233 | /** 234 | * Logger path constant 235 | * 236 | * {@inheritDoc} **Example:** 237 | * ```php 238 | * $mikro[Logger\PATH] = '/path/to/logs'; 239 | * ``` 240 | */ 241 | const PATH = 'Logger\PATH'; 242 | 243 | /** 244 | * Logger type constant 245 | * 246 | * {@inheritDoc} **Example:** 247 | * ```php 248 | * $mikro[Logger\TYPE] = 'daily'; 249 | * $mikro[Logger\TYPE] = 'single'; 250 | * ``` 251 | */ 252 | const TYPE = 'Logger\TYPE'; 253 | } 254 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 5, 'order' => 'title'] 62 | * ``` 63 | */ 64 | function query(): array 65 | { 66 | \parse_str(query_string(), $query); 67 | 68 | return $query; 69 | } 70 | 71 | /** 72 | * Gets all request data 73 | * 74 | * {@inheritDoc} **Example:** 75 | * ```php 76 | * Request\all(); // array ['foo' => 5, 'bar' => 'baz'] 77 | * ``` 78 | */ 79 | function all(): array 80 | { 81 | return \array_merge($_REQUEST, $_FILES, to_array()); 82 | } 83 | 84 | /** 85 | * Gets request data 86 | * 87 | * {@inheritDoc} **Example:** 88 | * ```php 89 | * Request\get('foo'); // string 'bar' 90 | * Request\get('value', 'default'); // string 'default' 91 | * ``` 92 | */ 93 | function get(string $key, mixed $default = null): mixed 94 | { 95 | return $_REQUEST[$key] ?? $_FILES[$key] ?? to_array()[$key] ?? $default; 96 | } 97 | 98 | /** 99 | * Gets request data 100 | * 101 | * {@inheritDoc} **Example:** 102 | * ```php 103 | * Request\input('foo'); // string 'bar' 104 | * Request\input('value', 'default'); // string 'default' 105 | * ``` 106 | */ 107 | function input(string $key, mixed $default = null): mixed 108 | { 109 | return get($key, $default); 110 | } 111 | 112 | /** 113 | * Gets request body 114 | */ 115 | function content(): string 116 | { 117 | return \file_get_contents('php://input'); 118 | } 119 | 120 | /** 121 | * Parse request body to array 122 | * 123 | * {@inheritDoc} **Example:** 124 | * ```php 125 | * Request\to_array(); // array 126 | * ``` 127 | */ 128 | function to_array(): array 129 | { 130 | $content = content(); 131 | 132 | if (header('Content-Type') === 'application/x-www-form-urlencoded') { 133 | \parse_str($content, $array); 134 | } else { 135 | $array = (array) \json_decode($content); 136 | } 137 | 138 | return $array; 139 | } 140 | 141 | /** 142 | * Gets header 143 | * 144 | * {@inheritDoc} **Example:** 145 | * ```php 146 | * Request\header('Content-type'); // string 'text/html' 147 | * ``` 148 | */ 149 | function header(string $key, mixed $default = null): mixed 150 | { 151 | return headers()[$key] ?? $default; 152 | } 153 | 154 | /** 155 | * Gets all header data 156 | * 157 | * {@inheritDoc} **Example:** 158 | * ```php 159 | * Request\headers(); // array ['Content-type' => '..'] 160 | * ``` 161 | */ 162 | function headers(): array 163 | { 164 | return \function_exists('getallheaders') ? \getallheaders() : []; 165 | } 166 | 167 | /** 168 | * Determine if the current request is asking for JSON. 169 | * 170 | * {@inheritDoc} **Example:** 171 | * ```php 172 | * Request\wants_json(); // bool 173 | * ``` 174 | */ 175 | function wants_json(): bool 176 | { 177 | $header = header('Accept', ''); 178 | $pieces = \explode(',', $header); 179 | $first = $pieces[0] ?? ''; 180 | 181 | return \str_contains($first, '/json') || \str_contains($first, '+json'); 182 | } 183 | 184 | /** 185 | * Get bearer token. 186 | * 187 | * {@inheritDoc} **Example:** 188 | * ```php 189 | * Request\bearer_token(); // token string 190 | * ``` 191 | */ 192 | function bearer_token(): ?string 193 | { 194 | $auth = header('Authorization'); 195 | 196 | return $auth ? \preg_replace('/^Bearer /', '', $auth) : null; 197 | } 198 | 199 | /** 200 | * Returns certain parameters. 201 | * 202 | * {@inheritDoc} **Example:** 203 | * ```php 204 | * Request\only(['param1', 'param2']); // array 205 | * ``` 206 | */ 207 | function only(array $keys): array 208 | { 209 | return \array_intersect_key(all(), \array_flip($keys)); 210 | } 211 | 212 | /** 213 | * Returns it by excluding certain parameters. 214 | * 215 | * {@inheritDoc} **Example:** 216 | * ```php 217 | * Request\except(['param3', 'param2']); // array 218 | * ``` 219 | */ 220 | function except(array $keys): array 221 | { 222 | return \array_diff_key(all(), \array_flip($keys)); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 'http://foo']); 16 | * ``` 17 | */ 18 | function output( 19 | ?string $content = null, 20 | int $code = STATUS['HTTP_OK'], 21 | array $headers = [] 22 | ): int { 23 | foreach ($headers as $key => $value) { 24 | header($key, $value); 25 | } 26 | 27 | \http_response_code($code); 28 | 29 | return print($content); 30 | } 31 | 32 | /** 33 | * Output a html response 34 | * 35 | * {@inheritDoc} **Example:** 36 | * ```php 37 | * Response\html('Hello world!'); 38 | * Response\html('404 page not found!', Response\STATUS['HTTP_NOT_FOUND']); 39 | * Response\html('Error!', Response\STATUS['HTTP_INTERNAL_SERVER_ERROR']); 40 | * ``` 41 | */ 42 | function html( 43 | string $content, 44 | int $code = STATUS['HTTP_OK'], 45 | array $headers = [] 46 | ): int { 47 | if (! \array_key_exists('Content-Type', $headers)) { 48 | $headers['Content-Type'] = 'text/html;charset=utf-8'; 49 | } 50 | 51 | return output($content, $code, $headers); 52 | } 53 | 54 | /** 55 | * Output a json response 56 | * 57 | * {@inheritDoc} **Example:** 58 | * ```php 59 | * Response\json(['status' => true]); 60 | * Response\json(['status' => false], Response\STATUS['HTTP_NOT_FOUND']); 61 | * Response\json(['status' => false], Response\STATUS['HTTP_INTERNAL_SERVER_ERROR']); 62 | * ``` 63 | */ 64 | function json( 65 | mixed $content, 66 | int $code = STATUS['HTTP_OK'], 67 | array $headers = [] 68 | ): int { 69 | if (! \array_key_exists('Content-Type', $headers)) { 70 | $headers['Content-Type'] = 'application/json;charset=utf-8'; 71 | } 72 | 73 | $json = (string) \json_encode($content); 74 | 75 | return output($json, $code, $headers); 76 | } 77 | 78 | /** 79 | * Output a text response 80 | * 81 | * {@inheritDoc} **Example:** 82 | * ```php 83 | * Response\text('Hello world!'); 84 | * Response\text('404 page not found!', Response\STATUS['HTTP_NOT_FOUND']); 85 | * Response\text('Error!', Response\STATUS['HTTP_INTERNAL_SERVER_ERROR']); 86 | * ``` 87 | */ 88 | function text( 89 | string $content, 90 | int $code = STATUS['HTTP_OK'], 91 | array $headers = [] 92 | ): int { 93 | if (! \array_key_exists('Content-Type', $headers)) { 94 | $headers['Content-Type'] = 'text/plain;charset=utf-8'; 95 | } 96 | 97 | return output($content, $code, $headers); 98 | } 99 | 100 | /** 101 | * Redirect response 102 | * 103 | * {@inheritDoc} **Example:** 104 | * ```php 105 | * Response\redirect('url'); 106 | * Response\redirect('url', STATUS['HTTP_PERMANENTLY_REDIRECT']); 107 | * ``` 108 | */ 109 | function redirect(string $to, int $code = STATUS['HTTP_MOVED_PERMANENTLY']): void 110 | { 111 | \http_response_code($code); 112 | 113 | header('Location', $to); 114 | } 115 | 116 | /** 117 | * Redirect response to referer url 118 | * 119 | * {@inheritDoc} **Example:** 120 | * ```php 121 | * Response\redirect_back(); 122 | * ``` 123 | */ 124 | function redirect_back(int $code = STATUS['HTTP_MOVED_PERMANENTLY']): void 125 | { 126 | \http_response_code($code); 127 | $referer = $_SERVER['HTTP_REFERER'] ?? '/'; 128 | 129 | header('Location', $referer); 130 | } 131 | 132 | /** 133 | * Render view and output html response 134 | * 135 | * {@inheritDoc} **Example:** 136 | * ```php 137 | * Response\view('view_file'); 138 | * Response\view('view_file', ['data' => 'foo']); 139 | * Response\view('errors/404', ['data' => 'foo'], STATUS['HTTP_NOT_FOUND']); 140 | * Response\view('errors/500', [], STATUS['HTTP_INTERNAL_SERVER_ERROR'], [...$headers]); 141 | * ``` 142 | */ 143 | function view( 144 | string $file, 145 | array $data = [], 146 | int $code = STATUS['HTTP_OK'], 147 | array $headers = [] 148 | ): int { 149 | return html(View\render($file, $data), $code, $headers); 150 | } 151 | 152 | /** 153 | * Send header with key/value 154 | * 155 | * {@inheritDoc} **Example:** 156 | * ```php 157 | * Response\header('Content-type', 'text/html'); 158 | * Response\header('Location', 'url'); 159 | * ``` 160 | */ 161 | function header(string $key, string $value, mixed ...$args): void 162 | { 163 | \header(\sprintf('%s:%s', $key, $value), ...$args); 164 | } 165 | 166 | /** 167 | * Output a success response (status code: 200) 168 | * 169 | * {@inheritDoc} **Example:** 170 | * ```php 171 | * Response\ok(['response_type' => 'JSON']); 172 | * Response\ok('Response type: HTML'); 173 | * ``` 174 | */ 175 | function ok(mixed $data, int $code = STATUS['HTTP_OK'], array $headers = []): int 176 | { 177 | if (\is_array($data) || \is_object($data)) { 178 | return json($data, $code, $headers); 179 | } 180 | 181 | return html($data, $code, $headers); 182 | } 183 | 184 | /** 185 | * Output a fail response (status code: 500) 186 | * 187 | * {@inheritDoc} **Example:** 188 | * ```php 189 | * Response\error(['response_type' => 'JSON']); 190 | * Response\error('Response type: HTML'); 191 | * Response\error('error details', Response\STATUS['HTTP_NOT_FOUND']); 192 | * ``` 193 | */ 194 | function error(mixed $data, int $code = STATUS['HTTP_INTERNAL_SERVER_ERROR'], array $headers = []): int 195 | { 196 | if (\is_array($data) || \is_object($data)) { 197 | return json($data, $code, $headers); 198 | } 199 | 200 | return html($data, $code, $headers); 201 | } 202 | 203 | /** 204 | * Http status codes 205 | * 206 | * {@inheritDoc} **Example:** 207 | * ```php 208 | * Response\json($data, Response\STATUS['HTTP_OK']); 209 | * Response\html($data, Response\STATUS['HTTP_NOT_FOUND']); 210 | * Response\view('pages/error', ['error' => $e], Response\STATUS['HTTP_INTERNAL_SERVER_ERROR']); 211 | * ``` 212 | */ 213 | const STATUS = [ 214 | 'HTTP_CONTINUE' => 100, 215 | 'HTTP_SWITCHING_PROTOCOLS' => 101, 216 | 'HTTP_PROCESSING' => 102, 217 | 'HTTP_EARLY_HINTS' => 103, 218 | 'HTTP_OK' => 200, 219 | 'HTTP_CREATED' => 201, 220 | 'HTTP_ACCEPTED' => 202, 221 | 'HTTP_NON_AUTHORITATIVE_INFORMATION' => 203, 222 | 'HTTP_NO_CONTENT' => 204, 223 | 'HTTP_RESET_CONTENT' => 205, 224 | 'HTTP_PARTIAL_CONTENT' => 206, 225 | 'HTTP_MULTI_STATUS' => 207, 226 | 'HTTP_ALREADY_REPORTED' => 208, 227 | 'HTTP_IM_USED' => 226, 228 | 'HTTP_MULTIPLE_CHOICES' => 300, 229 | 'HTTP_MOVED_PERMANENTLY' => 301, 230 | 'HTTP_FOUND' => 302, 231 | 'HTTP_SEE_OTHER' => 303, 232 | 'HTTP_NOT_MODIFIED' => 304, 233 | 'HTTP_USE_PROXY' => 305, 234 | 'HTTP_RESERVED' => 306, 235 | 'HTTP_TEMPORARY_REDIRECT' => 307, 236 | 'HTTP_PERMANENTLY_REDIRECT' => 308, 237 | 'HTTP_BAD_REQUEST' => 400, 238 | 'HTTP_UNAUTHORIZED' => 401, 239 | 'HTTP_PAYMENT_REQUIRED' => 402, 240 | 'HTTP_FORBIDDEN' => 403, 241 | 'HTTP_NOT_FOUND' => 404, 242 | 'HTTP_METHOD_NOT_ALLOWED' => 405, 243 | 'HTTP_NOT_ACCEPTABLE' => 406, 244 | 'HTTP_PROXY_AUTHENTICATION_REQUIRED' => 407, 245 | 'HTTP_REQUEST_TIMEOUT' => 408, 246 | 'HTTP_CONFLICT' => 409, 247 | 'HTTP_GONE' => 410, 248 | 'HTTP_LENGTH_REQUIRED' => 411, 249 | 'HTTP_PRECONDITION_FAILED' => 412, 250 | 'HTTP_REQUEST_ENTITY_TOO_LARGE' => 413, 251 | 'HTTP_REQUEST_URI_TOO_LONG' => 414, 252 | 'HTTP_UNSUPPORTED_MEDIA_TYPE' => 415, 253 | 'HTTP_REQUESTED_RANGE_NOT_SATISFIABLE' => 416, 254 | 'HTTP_EXPECTATION_FAILED' => 417, 255 | 'HTTP_I_AM_A_TEAPOT' => 418, 256 | 'HTTP_MISDIRECTED_REQUEST' => 421, 257 | 'HTTP_UNPROCESSABLE_ENTITY' => 422, 258 | 'HTTP_LOCKED' => 423, 259 | 'HTTP_FAILED_DEPENDENCY' => 424, 260 | 'HTTP_TOO_EARLY' => 425, 261 | 'HTTP_UPGRADE_REQUIRED' => 426, 262 | 'HTTP_PRECONDITION_REQUIRED' => 428, 263 | 'HTTP_TOO_MANY_REQUESTS' => 429, 264 | 'HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE' => 431, 265 | 'HTTP_UNAVAILABLE_FOR_LEGAL_REASONS' => 451, 266 | 'HTTP_INTERNAL_SERVER_ERROR' => 500, 267 | 'HTTP_NOT_IMPLEMENTED' => 501, 268 | 'HTTP_BAD_GATEWAY' => 502, 269 | 'HTTP_SERVICE_UNAVAILABLE' => 503, 270 | 'HTTP_GATEWAY_TIMEOUT' => 504, 271 | 'HTTP_VERSION_NOT_SUPPORTED' => 505, 272 | 'HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL' => 506, 273 | 'HTTP_INSUFFICIENT_STORAGE' => 507, 274 | 'HTTP_LOOP_DETECTED' => 508, 275 | 'HTTP_NOT_EXTENDED' => 510, 276 | 'HTTP_NETWORK_AUTHENTICATION_REQUIRED' => 511, 277 | ]; 278 | } 279 | -------------------------------------------------------------------------------- /src/Router.php: -------------------------------------------------------------------------------- 1 | [new HomeController(), 'index']()); 22 | * Router\map('GET|POST|PUT', '/posts/{id:num}', fn() => print('Post ID: ' . Router\parameters('id'))); 23 | * ``` 24 | */ 25 | function map( 26 | array|string $methods, 27 | string $path, 28 | mixed $callback, 29 | array|string $middleware = [] 30 | ): void { 31 | global $mikro; 32 | 33 | if (\is_string($methods)) { 34 | $methods = \explode('|', $methods); 35 | } 36 | 37 | $path = ($mikro[PREFIX] ?? '') . $path; 38 | 39 | if (\is_string($middleware)) { 40 | $middleware = \explode('|', $middleware); 41 | } 42 | 43 | $middleware = \array_merge($mikro[MIDDLEWARE] ?? [], $middleware); 44 | $requestPath = \rawurldecode(\rtrim(Request\path(), '/') ?: '/'); 45 | 46 | if (\in_array(Request\method(), $methods) && $requestPath === $path) { 47 | goto found; 48 | } 49 | 50 | $path = \rtrim(parse_path($path), '/'); 51 | 52 | if ( 53 | \in_array(Request\method(), $methods) && 54 | (\preg_match(\sprintf('@^%s$@i', $path), $requestPath, $params) >= 1) && 55 | ($mikro[FOUND] ?? null) !== true 56 | ) { 57 | found: 58 | $mikro[FOUND] = true; 59 | 60 | if (isset($params)) { 61 | \array_shift($params); 62 | $mikro[PARAMETERS] = $params; 63 | } 64 | 65 | $result = \array_reduce(\array_reverse($middleware), function ($stack, $item) { 66 | return function () use ($stack, $item) { 67 | return $item($stack); 68 | }; 69 | }, $callback); 70 | 71 | \call_user_func($result); 72 | } 73 | } 74 | 75 | /** 76 | * Parse route parameters 77 | * 78 | * @internal 79 | */ 80 | function parse_path(string $path): string 81 | { 82 | if (\preg_match('/(\/{.*}\?)/i', $path, $matches)) { 83 | foreach (\range(1, \count($matches)) as $match) { 84 | $path = \preg_replace('/\/({.*}\?)/', '/?$1', $path); 85 | } 86 | } 87 | 88 | \preg_replace_callback('/[\[{\(].*[\]}\)]/U', function ($match) use (&$path): string { 89 | $match = \str_replace(['{', '}'], '', $match[0]); 90 | 91 | if (\str_contains($match, ':')) { 92 | [$name, $type] = \explode(':', $match, 2); 93 | } else { 94 | $name = $match; 95 | $type = 'any'; 96 | } 97 | 98 | $patterns = [ 99 | 'num' => '(?\d+)', 100 | 'str' => '(?[\w\-_]+)', 101 | 'any' => '(?[^/]+)', 102 | 'all' => '(?.*)', 103 | ]; 104 | $replaced = \str_replace('name', $name, ($patterns[$type] ?? $patterns['any'])); 105 | $path = \str_replace("{{$name}:$type}", $replaced, $path); 106 | $path = \str_replace("{{$name}}", $replaced, $path); 107 | 108 | return $path; 109 | }, $path); 110 | 111 | return $path; 112 | } 113 | 114 | /** 115 | * Maps the GET route 116 | * 117 | * {@inheritDoc} **Example:** 118 | * ```php 119 | * Router\get('/', 'callback'); 120 | * Router\get('/', 'callback', $middlewareArray); 121 | * Router\get('/', function () { 122 | * return Response\view('home'); 123 | * }); 124 | * ``` 125 | */ 126 | function get(string $path, mixed $callback, array|string $middleware = []): void 127 | { 128 | map('GET', $path, $callback, $middleware); 129 | } 130 | 131 | /** 132 | * Maps the POST route 133 | * 134 | * {@inheritDoc} **Example:** 135 | * ```php 136 | * Router\post('/save', 'callback'); 137 | * Router\post('/', 'callback', $middlewareArray); 138 | * Router\post('/posts', function () { 139 | * return DB\insert('posts', ['title' => Request\get('title')]); 140 | * }); 141 | * ``` 142 | * 143 | */ 144 | function post(string $path, mixed $callback, array|string $middleware = []): void 145 | { 146 | map('POST', $path, $callback, $middleware); 147 | } 148 | 149 | /** 150 | * Maps the PATCH route 151 | * 152 | * {@inheritDoc} **Example:** 153 | * ```php 154 | * Router\patch('/update', 'callback'); 155 | * Router\patch('/update', 'callback', $middlewareArray); 156 | * ``` 157 | */ 158 | function patch(string $path, mixed $callback, array|string $middleware = []): void 159 | { 160 | map('PATCH', $path, $callback, $middleware); 161 | } 162 | 163 | /** 164 | * Maps the PUT route 165 | * 166 | * {@inheritDoc} **Example:** 167 | * ```php 168 | * Router\put('/update', 'callback'); 169 | * Router\put('/update', 'callback', $middlewareArray); 170 | * ``` 171 | */ 172 | function put(string $path, mixed $callback, array|string $middleware = []): void 173 | { 174 | map('PUT', $path, $callback, $middleware); 175 | } 176 | 177 | /** 178 | * Maps the DELETE route 179 | * 180 | * {@inheritDoc} **Example:** 181 | * ```php 182 | * Router\delete('/destroy', 'callback'); 183 | * Router\delete('/destroy', 'callback', $middlewareArray); 184 | * ``` 185 | */ 186 | function delete(string $path, mixed $callback, array|string $middleware = []): void 187 | { 188 | map('DELETE', $path, $callback, $middleware); 189 | } 190 | 191 | /** 192 | * Maps the OPTIONS route 193 | * 194 | * {@inheritDoc} **Example:** 195 | * ```php 196 | * Router\options('/', 'callback'); 197 | * Router\options('/', 'callback', $middlewareArray); 198 | * ``` 199 | */ 200 | function options(string $path, mixed $callback, array|string $middleware = []): void 201 | { 202 | map('OPTIONS', $path, $callback, $middleware); 203 | } 204 | 205 | /** 206 | * Maps the any route 207 | * 208 | * {@inheritDoc} **Example:** 209 | * ```php 210 | * Router\any('/{anything:any}', 'callback'); 211 | * Router\any('/{anything:any}', 'callback', $middlewareArray); 212 | * ``` 213 | */ 214 | function any(string $path, mixed $callback, array|string $middleware = []): void 215 | { 216 | map('GET|POST|PATCH|PUT|DELETE', $path, $callback, $middleware); 217 | } 218 | 219 | /** 220 | * Maps the view route 221 | * 222 | * {@inheritDoc} **Example:** 223 | * ```php 224 | * Router\view('/page', 'templates/page'); 225 | * Router\view('/page', 'templates/page', ['with' => 'data']); 226 | * ``` 227 | */ 228 | function view(string $path, string $file, array $data = [], array|string $middleware = []): void 229 | { 230 | any($path, fn() => Response\view($file, $data), $middleware); 231 | } 232 | 233 | /** 234 | * Maps the redirect route 235 | * 236 | * {@inheritDoc} **Example:** 237 | * ```php 238 | * Router\redirect('/page', '/new/page/url'); 239 | * Router\redirect('/other-page', 'https://url'); 240 | * ``` 241 | */ 242 | function redirect(string $path, string $to, array|string $middleware = []): void 243 | { 244 | any($path, fn() => Response\redirect($to), $middleware); 245 | } 246 | 247 | /** 248 | * Checks route is match. It must be located at the end of all route definitions. 249 | * 250 | * {@inheritDoc} **Example:** 251 | * ```php 252 | * if (! Router\is_found()) { 253 | * echo '404 not found!'; 254 | * } 255 | * ``` 256 | */ 257 | function is_found(): bool 258 | { 259 | global $mikro; 260 | 261 | return isset($mikro[FOUND]) && $mikro[FOUND] === true; 262 | } 263 | 264 | /** 265 | * Define 404 error route. It must be located at the end of all route definitions. 266 | * 267 | * {@inheritDoc} **Example:** 268 | * ```php 269 | * Router\error('callback'); 270 | * Router\error(function () { 271 | * Response\html('404 not found!', Response\STATUS['HTTP_NOT_FOUND']); 272 | * }); 273 | * Router\error([ 274 | * fn() => Response\html('Default 404 error handler'), 275 | * '/posts' => fn() => Response\html('/posts 404 error handler'), 276 | * '/products' => fn() => 'ProductController::notFoundHandler', 277 | * ]) 278 | * ``` 279 | */ 280 | function error(mixed $callback = []): void 281 | { 282 | if (! \is_array($callback)) { 283 | $callback = [$callback]; 284 | } 285 | 286 | if (! is_found()) { 287 | \http_response_code(404); 288 | $path = Request\path(); 289 | 290 | foreach ($callback as $key => $_callback) { 291 | $starts = \str_starts_with($path, (string) $key); 292 | $match = \preg_match('@' . (string) $key . '@', $path); 293 | 294 | if (! empty($key) && ($starts || $match)) { 295 | if (! \is_callable($_callback)) { 296 | throw new ValidatorException("Error callback `$key` is not valid"); 297 | } 298 | 299 | \call_user_func($_callback); 300 | 301 | return; 302 | } 303 | } 304 | 305 | if (isset($callback[0]) && \is_callable($callback[0])) { 306 | \call_user_func($callback[0]); 307 | } 308 | } 309 | } 310 | 311 | /** 312 | * Group routes 313 | * 314 | * {@inheritDoc} **Example:** 315 | * ```php 316 | * Route\group('/admin', function () { 317 | * Route\get('', 'Admin\HomeController::index'); 318 | * 319 | * Route\group('/posts', function () { 320 | * Route\get('', 'Admin\PostController::index'); 321 | * Route\get('/{id:num}', 'Admin\PostController::show'); 322 | * }); 323 | * }); 324 | * Router\group('/prefix', fn() => [ 325 | * Router\get('/home', 'home_callback') 326 | * ], $middlewareArray); 327 | * ``` 328 | */ 329 | function group(string $prefix, mixed $callback, array|string $middleware = []): void 330 | { 331 | global $mikro; 332 | 333 | if (\is_string($middleware)) { 334 | $middleware = \explode('|', $middleware); 335 | } 336 | 337 | if (! isset($mikro[PREFIX])) { 338 | $mikro[PREFIX] = ''; 339 | } 340 | 341 | if (! isset($mikro[MIDDLEWARE])) { 342 | $mikro[MIDDLEWARE] = []; 343 | } 344 | 345 | $mikro[PREFIX] .= $prefix; 346 | $mikro[MIDDLEWARE] = \array_merge($mikro[MIDDLEWARE], $middleware); 347 | 348 | \call_user_func($callback); 349 | 350 | if (($pos = \strrpos($mikro[PREFIX], $prefix)) !== false) { 351 | $mikro[PREFIX] = \substr_replace($mikro[PREFIX], '', $pos, \strlen($prefix)); 352 | } 353 | 354 | foreach ($middleware as $mw) { 355 | \array_pop($mikro[MIDDLEWARE]); 356 | } 357 | } 358 | 359 | /** 360 | * Get route parameter(s) 361 | * 362 | * {@inheritDoc} **Example:** 363 | * ```php 364 | * Route\get('/posts/{postTitle}', function () { 365 | * Router\parameters(); // ['postTitle' => 'value'] 366 | * Router\parameters('postTitle') => 'value' 367 | * }); 368 | * 369 | * Route\get('/posts/{postId:num}', function () { 370 | * Router\parameters(); // ['postId' => '5'] 371 | * Router\parameters('postId') => '5' 372 | * }); 373 | * ``` 374 | * 375 | * Available options: num, str, any, all 376 | * 377 | * /posts/{post:num} => /posts/5 378 | * /posts/{post:str} => /posts/lorem-lipsum-dolor 379 | * /posts/{post:any} => /posts/lorem-lipsum_^54-any-char 380 | * /posts/{post:all} => /posts/any-char/any-slash 381 | * /posts/{post} => if no option, equals any option 382 | */ 383 | function parameters(?string $name = null, mixed $default = null): mixed 384 | { 385 | global $mikro; 386 | 387 | return $name === null ? 388 | $mikro[PARAMETERS] ?? [] : 389 | ($mikro[PARAMETERS][$name] ?? $default); 390 | } 391 | 392 | /** 393 | * Create resource routes 394 | * 395 | * {@inheritDoc} **Example:** 396 | * ```php 397 | * Router\resource('/posts', 'Controllers\PostController'); 398 | * Router\resource('/items', 'Controllers\ItemController:index|show'); // only index and show 399 | * Router\resource('/foo', FooController::class, ['middleware_one']); 400 | * ``` 401 | */ 402 | function resource(string $path, string $class, array|string $middleware = []): void 403 | { 404 | $only = null; 405 | 406 | if (\str_contains($class, ':')) { 407 | [$class, $only] = \explode(':', $class, 2); 408 | } 409 | 410 | $call = fn(string $method) => 411 | (\is_callable("{$class}::{$method}") ? 412 | "{$class}::{$method}" : fn() => \call_user_func([new $class(), $method])); 413 | 414 | $methods = [ 415 | 'index' => fn() => get($path, $call('index'), $middleware), 416 | 'store' => fn() => post($path, $call('store'), $middleware), 417 | 'create' => fn() => get("{$path}/create", $call('create'), $middleware), 418 | 'show' => fn() => get("{$path}/{id}", $call('show'), $middleware), 419 | 'edit' => fn() => get("{$path}/{id}/edit", $call('edit'), $middleware), 420 | 'update' => fn() => put("{$path}/{id}", $call('update'), $middleware), 421 | 'destroy' => fn() => delete("{$path}/{id}", $call('destroy'), $middleware), 422 | ]; 423 | 424 | if ($only) { 425 | foreach (\explode('|', $only) as $method) { 426 | if (isset($methods[$method])) { 427 | $methods[$method](); 428 | } 429 | } 430 | } else { 431 | foreach ($methods as $method) { 432 | $method(); 433 | } 434 | } 435 | } 436 | 437 | /** 438 | * Sync routes with files in specific directory 439 | * 440 | * {@inheritDoc} **Example:** 441 | * ```php 442 | * Router\files('/', __DIR__ . '/pages/front'); 443 | * Router\files('/admin', __DIR__ . '/pages/back', ['AdminMiddleware::handle']); 444 | * ``` 445 | */ 446 | function files(string $routePath, string $filesPath, array|string $middleware = []): void 447 | { 448 | $iterator = new \RecursiveIteratorIterator( 449 | new \RecursiveDirectoryIterator($filesPath) 450 | ); 451 | 452 | foreach ($iterator as $item) { 453 | if ($item->isDir() || ! \str_ends_with($item->getFilename(), '.php')) { 454 | continue; 455 | } 456 | 457 | $route = resolve_file($item, \realpath($filesPath)); 458 | 459 | map( 460 | [$route['method']], 461 | '/' . \trim($routePath . $route['path'], '/'), 462 | fn() => require($item->getPathName()), 463 | $middleware 464 | ); 465 | } 466 | } 467 | 468 | /** 469 | * Resolve file for route 470 | * 471 | * @internal 472 | */ 473 | function resolve_file(\SplFileInfo $file, string $base): array 474 | { 475 | $prefix = \str_replace([$base, \DIRECTORY_SEPARATOR], ['', '/'], \dirname($file->getRealPath())); 476 | $method = 'GET'; 477 | $path = $file->getBaseName('.php'); 478 | 479 | foreach (['get', 'post', 'put', 'patch', 'delete', 'options'] as $item) { 480 | if (\str_ends_with($file->getBaseName('.php'), $item)) { 481 | $method = \strtoupper($item); 482 | $path = $file->getBaseName('.' . $item . '.php'); 483 | } 484 | } 485 | 486 | $path = $path === 'index' ? '' : $path; 487 | $path = \rtrim($prefix . '/' . $path, '/') ?: '/'; 488 | 489 | return \compact('method', 'path'); 490 | } 491 | 492 | /** 493 | * Router found constant 494 | * 495 | * @internal 496 | */ 497 | const FOUND = 'Router\FOUND'; 498 | 499 | /** 500 | * Router prefix constant 501 | * 502 | * @internal 503 | */ 504 | const PREFIX = 'Router\PREFIX'; 505 | 506 | /** 507 | * Router middleware constant 508 | * 509 | * @internal 510 | */ 511 | const MIDDLEWARE = 'Router\MIDDLEWARE'; 512 | 513 | /** 514 | * Router parameters 515 | * 516 | * @internal 517 | */ 518 | const PARAMETERS = 'Router\PARAMETERS'; 519 | } 520 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | isset($data['title']) && ! empty($value); 14 | * Validator\validate($_POST, 'title', [$callback]); // required and filled 15 | * Validator\valdiate(Request\all(), 'username', [$callback, 'ctype_alnum']); 16 | * Validator\validate(Request\all(), 'email', 'filter_var:' . FILTER_VALIDATE_EMAIL); 17 | * ``` 18 | */ 19 | function validate(array $data, string $key, string|array|callable $rules): bool 20 | { 21 | // Split $rules is a string 22 | if (\is_string($rules)) { 23 | $rules = \explode('|', $rules); 24 | } 25 | 26 | // Cast $rules to array if that is a callable 27 | if (\is_callable($rules)) { 28 | $rules = [$rules]; 29 | } 30 | 31 | // Normalize rule 32 | $normalizeRule = function (string $rule) { 33 | $normalizedRule = []; 34 | 35 | // If data contains "!" character, set "not" key to true 36 | $normalizedRule['not'] = \str_contains($rule, '!') ? true : false; 37 | 38 | // If the rule contains the ":" 39 | if (\str_contains($rule, ':')) { 40 | // Split rule and parameters 41 | [$rule, $stringParameters] = \explode(':', $rule, 2); 42 | // Split parameters 43 | $parameters = \explode(',', $stringParameters); 44 | 45 | // Set splitted parameters 46 | $normalizedRule['parameters'] = $parameters; 47 | } else { 48 | // Parameter is not defined 49 | $normalizedRule['parameters'] = []; 50 | } 51 | 52 | // Normalize the rule. The rules can also have callback functions. 53 | $normalizedRule['rule'] = \str_replace('!', '', $rule); 54 | 55 | return $normalizedRule; 56 | }; 57 | 58 | $pass = \array_map(function ($rule) use ($normalizeRule, $data, $key) { 59 | if (\is_string($rule)) { 60 | $rule = $normalizeRule($rule); 61 | 62 | // isset and empty functions are not callback in PHP 63 | $result = match ($rule['rule']) { 64 | 'isset' => isset($data[$key]), 65 | 'empty' => empty($data[$key] ?? ''), 66 | default => \call_user_func_array( 67 | $rule['rule'], 68 | \array_merge([$data[$key] ?? null], $rule['parameters']) 69 | ) 70 | }; 71 | } elseif (\is_callable($rule)) { 72 | $result = \call_user_func_array($rule, [$data[$key] ?? null, $data]); 73 | } else { 74 | return false; 75 | } 76 | 77 | return (\is_array($rule) && (isset($rule['not']) ? $rule['not'] : false)) ? 78 | ! $result : $result; 79 | }, $rules); 80 | 81 | return empty(\array_filter($pass, fn($item) => $item === false)); 82 | } 83 | 84 | /** 85 | * Validate array and get all results in array 86 | * 87 | * {@inheritDoc} **Example:** 88 | * ```php 89 | * Validator\validate_all( 90 | * ['title' => 'foo', 'email' => 'bar'], 91 | * [ 92 | * 'title' => 'isset|!empty|ctype_alnum', 93 | * 'email' => 'isset|!empty|filter_var:' . FILTER_VALIDATE_EMAIL 94 | * ] 95 | * ); // ['title' => true, 'email' => false] 96 | * ``` 97 | */ 98 | function validate_all(array $data, array $rules): array 99 | { 100 | $results = \array_map(function ($key, $rule) use ($data) { 101 | return validate($data, $key, $rule); 102 | }, \array_keys($rules), \array_values($rules)); 103 | 104 | return \array_combine(\array_keys($rules), \array_values($results)); 105 | } 106 | 107 | /** 108 | * Validate array and get result in boolean 109 | * 110 | * {@inheritDoc} **Example:** 111 | * ```php 112 | * Validator\is_validate_all( 113 | * ['title' => 'foo', 'email' => 'bar'], 114 | * [ 115 | * 'title' => 'isset|!empty|ctype_alnum', 116 | * 'email' => 'isset|!empty|filter_var:' . FILTER_VALIDATE_EMAIL 117 | * ] 118 | * ); // true|false 119 | * ``` 120 | */ 121 | function is_validated_all(array $data, array $rules): bool 122 | { 123 | $results = \array_map(function ($key, $rule) use ($data) { 124 | return validate($data, $key, $rule); 125 | }, \array_keys($rules), \array_values($rules)); 126 | 127 | return empty(\array_filter($results, fn($result) => $result === false)); 128 | } 129 | 130 | /** 131 | * Validate request data and get all results in array 132 | * 133 | * {@inheritDoc} **Example:** 134 | * ```php 135 | * Validator\validate_request([ 136 | * 'title' => 'isset|!empty|ctype_alnum', 137 | * 'email' => 'isset|!empty|filter_var:' . FILTER_VALIDATE_EMAIL 138 | * ]); // ['title' => true, 'email' => false] 139 | * ``` 140 | */ 141 | function validate_request(array $rules): array 142 | { 143 | return validate_all(Request\all(), $rules); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/View.php: -------------------------------------------------------------------------------- 1 | 'bar']); 18 | * ``` 19 | * 20 | * @throws MikroException If view path not set on global $mikro array 21 | * @throws ViewException If view file not found 22 | */ 23 | function render(string $file, array $data = []): string 24 | { 25 | global $mikro; 26 | 27 | if (! isset($mikro[PATH])) { 28 | throw new MikroException('Please set the view path'); 29 | } 30 | 31 | if (($path = exists($file)) === false) { 32 | throw new ViewException("View file ({$file}) not found in: {$path}"); 33 | } 34 | 35 | \ob_start(); 36 | 37 | if (! empty($data)) { 38 | \extract($data); 39 | } 40 | 41 | require cache($path); 42 | 43 | return \ltrim((string) \ob_get_clean()); 44 | } 45 | 46 | /** 47 | * Escape string 48 | * 49 | * {@inheritDoc} **Example:** 50 | * ```php 51 | * echo View\e('