资源说明:Minimalist PHP toolkit
> Looking for previous version? Try [this branch](https://github.com/chromice/ae/tree/deadend-1.0). # æ – minimalist PHP toolkit æ (pronounced "ash") is a collection of loosely coupled PHP components for request routing, response caching, templating, form validation, image manipulation, database operations, and easy debugging and profiling. This project has been created by its sole author to explore, express and validate his views on web development. As a result, this is an opinionated codebase that attempts to achieve the following goals: - **Simplicity:** There are no controllers, event emitters and responders, filters, template engines. There are no config files to tinker with either: most components come preconfigured with sensible default values. - **Reliability**: The APIs were designed to be expressive and user error-resistant. All functionality described in this document is covered with tests. - **Performance:** All components have been designed with performance and efficiency in mind. Responses can be cached statically and served by Apache alone. - **Independence:** This toolkit does not have any third-party dependencies and the codebase is intentially small and clean, so that anyone can understand how something works, or why it does not work. There is nothing particularly groundbreaking or fancy about this toolkit. If you just need a lean PHP framework, you may have found it. However, if someone told you that all your code must be broken into models, views and controllers, you will be better off using something like [Yii](http://www.yiiframework.com) or [Laravel](http://laravel.com). æ will be perfect for you, if your definition of a web application falls along these lines: > A web application is a bunch of scripts thrown together to concatenate a string of text (HTTP response) in response to another string of text (HTTP request). In other words, æ will not let you forget that most of the back-end code is a glorified string concatenation, but it will alleviate the most cumbersome aspects of it. In more practical terms, if you are putting together a site with some forms that save data to a database, and then present that data back to the user on a bunch of pages, æ comes with everything you need. You may still find it useful, even if you are thinking of web app architecture in terms of dispatchers, controllers, events, filters, etc. The author assumes you are working on something complex and wishes you a hearty good luck! * * * ## Table of contents - [Getting started](#getting-started) - [Requirements](#requirements) - [Manual installation](#manual-installation) - [Configuring Composer](#configuring-composer) - [Hello world](#hello-world) - [Request](#request) - [Request mapping](#request-mapping) - [Response](#response) - [Buffer](#buffer) - [Template](#template) - [Layout](#layout) - [Cache](#cache) - [Path](#path) - [File](#file) - [Image](#image) - [Resizing and cropping](#resizing-and-cropping) - [Applying filters](#applying-filters) - [Conversion and saving](#conversion-and-saving) - [Form](#form) - [Declaration](#declaration) - [Validation](#validation) - [Presentation](#presentation) - [Complex field types](#complex-field-types) - [Database](#database) - [Making queries](#making-queries) - [Query functions](#query-functions) - [Active record](#active-record) - [Transactions](#transactions) - [Migrations](#migrations) - [Inspector](#inspector) - [Debugging](#debugging) - [Profiling](#profiling) - [Utilities](#utilities) - [Exception safety](#exception-safety) - [Configuration options](#configuration-options) - [Testing](./TESTING.md) - [License](./LICENSE.md) * * * ## Getting started ### Platform requirements - **PHP**: version 5.4 or higher with *GD extension* for image manipulation, and *Multibyte String extension* for form validation. - **MySQL**: version 5.1 or higher with *InnoDB engine*. - **Apache**: version 2.0 or higher with *mod_rewrite* for nice URLs, and (optionally) *mod_deflate* for response compression. ### Manual installation You can download the latest release manually, drop it into your project and `require` ae/core.php: ```php require 'path/to/ae/core.php'; ``` ### Configuring Composer If you are using [Composer](https://getcomposer.org), make sure your composer.json references this repository and has æ added as a requirement: ```json { "repositories": [ { "type": "vcs", "url": "https://github.com/chromice/ae" } ], "require": { "chromice/ae": "dev-develop" } } ``` ### Hello world Let's create the most basic of web applications. Create a file named index.php in the web root directory and paste this code into it: ```php ``` Now let's instruct Apache to redirect all unresolved URIs to index.php, by adding the following rules to .htaccess file in the web root: ```apacheRewriteEngine on RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*) index.php?/$1 [L,QSA] ``` Now, if you open our app at, say, http://localhost/ in a browser, you should see this: ```txt GET / HTTP/1.1 Hello world! ``` If you change the address to http://localhost/universe, you should see: ```txt GET /universe HTTP/1.1 Hello universe! ``` And that is it! Let's familiarise you with all the components. ## Request Request component is a lightweight abstraction of HTTP requests that let's you do the following: - Distinguish between different request methods (i.e. `GET`, `POST`, `PUT`, `PATCH`, `DELETE`) via `\ae\request\method()` function: ```php if (\ae\request\method() === \ae\request\GET) { echo "This is a GET request."; } else if (\ae\request\method() === \ae\request\POST) { echo "This is a POST request."; } ``` - Access URI path segments via `\ae\request\path()` function directly or via path object it returns when called with no arguments: ```php // GET /some/arbitrary/script.php HTTP/1.1 $path = \ae\request\path(); $path; // 'some/arbitrary/script.php' $path[0]; // 'some' $path[1]; // 'arbitrary' $path[2]; // 'script.php' $path[-3]; // 'some' $path[-2]; // 'arbitrary' $path[-1]; // 'script.php' \ae\request\path(2); // 'script.php' \ae\request\path(3); // NULL \ae\request\path(3, 'fallback'); // 'fallback' ``` - Get the expected response extension (html by default), which is determined by the *extension* part of the URI path. ```php // GET /some/arbitrary/request.json HTTP/1.1 \ae\request\extension(); // 'json' ``` - Get the client IP address via `\ae\request\from()` function. > **N.B.** If your app is running behind a reverse-proxy and/or load balancer, you must specify their IP addresses first ```php \ae\request\configure('proxies', ['83.14.1.1', '83.14.1.2']); $client_ip = \ae\request\from(); ``` - Access `$_GET` query arguments and `$_POST` data via `\ae\request\query()` and `\ae\request\data()` functions respectively: ```php // POST /search HTTP/1.1 // // term=foo $get = \ae\request\query(); // $_GET $post = \ae\request\data(); // $_POST \ae\request\query('action', 'search'); // 'search' \ae\request\data('term'); // 'foo' ``` - Access uploaded files (when request body is encoded as multipart/form-data) via `\ae\request\files()` function. ```php $files = \ae\request\files(); // array // e.g. ['form_input_name' => \ae\file(), ...] $foo_file = \ae\request\file('foo'); // \ae\file($_FILES['foo']['tmp_name']) ``` - Get a request header via `\ae\request\header()` function: ```php $headers = \ae\request\header(); // array $charset = \ae\request\header('Accept-Charset'); // $headers['Accept-Charset'] ``` - Access raw request body, use `\ae\request\body()` function: ```php $raw = \ae\request\body(); // file_get_contents('php://input') ``` - Map requests to a function/method or instance of a class that implements `\ae\response\Dispatchable` interface. ### Request mapping You should always strive to break down your application into smallest independent components. The best way to handle a request is to map it to a specific function or template that encapsulates a part of your application's functionality. Requests are mapped using rules, key-value pairs of a path pattern and *either* an object that conforms to `\ae\response\Dispatchable` interface *or* a function/method that returns such an object. Here's an example of a request (GET /about-us HTTP/1.1) being mapped to a page template (about-us-page.php): ```php // GET /about-us HTTP/1.1 \ae\request\map([ // ... '/about-us' => \ae\template('path/to/about-us-page.php'), '/our-work' => \ae\template('path/to/our-work-page.php'), // ... ]); ``` If the template file does not exist, `\ae\template()` will throw an `\ae\path\Exception`, which in turn will result in `\ae\response\error(404)` being dispatched. > **N.B.** If a request handler throws `\ae\path\Exception`, `\ae\response\error(404)` is dispatched. If it throws any other exception, `\ae\response\error(500)` is dispatched instead. Now, let's enable users to download files from a specific directory: ```php // GET /download/directory/document.pdf HTTP/1.1 \ae\request\map([ // ... '/download' => function ($file_path) { return \ae\file('path/to/downloadable/files/' . $file_path) // returns '/path/to/downloadable/files/directory/document.pdf' file, if it exists ->attachment(); }, // ... ]); ``` First of all, we take advantage of the fact that `\ae\file()` function returns an object that conforms to `\ae\response\Dispatchable` interface. Secondly, whenever actual matched URI path is longer than the pattern, the remainder of it is passed as *last argument* to our handler. And thirdly, we use `attachment()` method to set Content-Disposition header to attachment, and force the download rather than simply pass the file content through. > You can pass a custom file name to `attachment()` method as the first argument, if you do not want to use the actual name of the file. Image processing is a very common problem that can be solved in multiple ways. Let's create a simple image processor that accepts an image file path, resizes it to predefined dimensions, and caches the result for 10 years: ```php // GET /resized/square/avatars/photo.jpg HTTP/1.1 \ae\request\map([ // ... '/resized/{alpha}' => function ($format, $path) { switch ($format) { case 'square': $width = 256; $height = 256; break; case 'thumbnail': $width = 400; $height = 600; break; default: return \ae\request\error(404); } return \ae\image('image/directory/'. $path) ->fill($width, $height) ->cache(10 * \ae\cache\year, \ae\cache\server); }, // ... ]); ``` Similarly to the file download example, the file path is passed as *the last argument* to our handler. In addition to that, we catch the image format as *the first argument*. The object returned by `\ae\image()` conforms to `\ae\response\Cachable` interface (in addition to `\ae\response\Dispatchable`) and implements `cache()` method. > There are also: `{numeric}` placeholder that matches and captures only numeric characters; `{any}` placeholder can match (and capture) a substring only within one path segment, i.e. it can match any character other than / (forward slash). Now let's write a more generic rule that handles all root level pages by using a placeholder, and mapping it to a function: ```php // GET /about-us HTTP/1.1 \ae\request\map([ // ... '/{any}' => function ($slug) { return \ae\template('path/to/' . $slug . '-page.php'); // returns '/path/to/about-us-page.php' template, it it exists }, // ... ]); ``` Here we use `{any}` placeholder to catch the slug of the page and pass its value to our handler function as the first argument. And finally, our last rule will display home page, *or* show 404 error for all unmatched requests by returning `null`: ```php // GET / HTTP/1.1 \ae\request\map([ // ... '/' => function ($path) { return empty($path) ? \ae\template('path/to/home-page.php') : null; } ]); ``` > **N.B.** All rules are processed in sequence. You should always put rules with higher specificity at the top. '/' is the least specific rule and will match *any* request. ## Response Response component is a set of functions, classes, and interfaces that lets you create a response object, set its content and headers, and (optionally) cache and compress it. It is designed to work with `\ae\request\map()` function (see above), which expects you to create a response object for each request. Here is an example of a simple application that creates a response, sets one custom header, caches it for 5 minutes, and dispatches it. The response is also automatically compressed using Apache's `mod_deflate`: ```php header('X-Header-Example', 'Some value'); ?>Hello world
cache(5 * \ae\cache\minute, \ae\cache\server) ->dispatch('hello-world.html'); ?> ``` When response object is created, it starts buffering all output. Once the `dispatch()` method is called, the buffering stops, HTTP headers are set, and content is output. > You must explicitly specify the response path when using `dispatch()` method. To create a response for the current request use `\ae\request\path()` function. By default all responses are text/html, but you can change the type by either setting Content-type header to a valid mime-type or appending an appropriate file extension to the dispatched path, e.g. .html, .css, .js, .json > **N.B.** Objects returned by `\ae\response()`, `\ae\buffer()`, `\ae\template()`, `\ae\file()`, `\ae\image()` implement `\ae\response\Dispatchable` interface, which allows you to dispatch them. You should refrain from using `dispatch()` method explicitly though, and use the request mapping pattern described previously. ### Buffer You can create a buffer and assign it to a variable to start capturing output. All output is captured until the instance is destroyed: ```php $buffer = \ae\buffer(); echo "I'm buffered!"; $content = (string) $buffer; // "I'm buffered!" echo "I'm still buffered!"; unset($buffer); echo "And I'm not buffered!"; ``` > `\ae\buffer()` returns an instance of `\ae\Buffer` class, which implements `__toString()` magic method that always returns a string currently contained in the buffer. ### Template Use `\ae\template()` to capture output of a parameterized script: ```php echo \ae\template('path/to/template.php', [ 'title' => 'Example!', 'body' => 'Hello world!
' ]); ``` Provided the content of template.php is: ```php= $title ?> = $body ?> ``` ...the script will output: ```htmlExample! Hello world!
``` > `\ae\template()` returns an instance of `\ae\Template` class, which implements `__toString()` magic method that renders the template with specified parameters. ### Layout Layout component allows you to wrap output of a script with output of another script. The layout script is executed *last*, thus avoiding many problems of using separate header and footer scripts to keep the template code [DRY](http://en.wikipedia.org/wiki/DRY). Here's an example of a simple layout layout_html.php: ```html= $title ?> = $__content__ ?> ``` Another script hello_world.php can use it like this: ```php 'Layout example' ]); ?>Hello World!
``` When rendered, it will produce this: ```htmlLayout example Hello World!
``` ### Cache Objects returned by `\ae\response()`, `\ae\buffer()`, `\ae\template()`, `\ae\file()`, `\ae\image()` implement `\ae\response\Cacheable` interface, which allows you to call their `cache()` method to cache them for a number of minutes: - `->cache(60)` will simply set Cache-Control, Last-Modified, and Expires headers - `->cache(60, \ae\cache\server)` will save the response to the server-side cache as well You can also use cache functions directly: - Save any response manually using `\ae\cache\save()` function: ```php \ae\cache\save('hello-world.html', $response, 2 * \ae\cache\hour); ``` - Delete any cached response via `\ae\cache\delete()` function by passing full or partial URL to it: ```php \ae\cache\delete('hello-world.html'); ``` - Remove all *stale* cache entries via `\ae\cache\clean()`: ```php \ae\cache\clean(); ``` - Erase all cached data completely via `\ae\cache\purge()` function: ```php \ae\cache\purge(); ``` > **N.B.** Cache cleaning and purging can be resource-intensive and should not be performed while processing a regular user request. You should create a dedicated cron script or use some other job queueing mechanism for that. #### Configuration The responses are saved to cache directory (in the web root directory) by default. For caching to work correctly this directory must exist and be writable. You must also configure Apache to look for cached responses in that directory: 1. Put the following rules into .htaccess file in the web root directory: ```apacheRewriteEngine on RewriteBase / # Append ".html", if there is no extension... RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} !\.\w+$ RewriteRule ^(.*?)$ /$1.html [L] # ...and redirect to cache directory ("/cache") RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*?)\.(\w+)$ /cache/$1/index.$2/index.$2 [L,ENV=FROM_ROOT:1] ``` 2. And here are the rules that cache/.htaccess must contain: ```apacheRewriteEngine on # If no matching file found, redirect back to index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*) /index.php?/$1 [L,QSA] ``` With everything in place, Apache will first look for an unexpired cached response, and only if it finds nothing, will it route the request to index.php in the web root directory. You can change cache directory location like this: ```php \ae\cache\configure('directory', 'path/to/cache'); ``` ## Path All functions thar accept relative file paths as an argument rely on path component to resolve them. By default, all paths are resolved relative to the location of your main script. But you are encouraged to explicitly specify the root directory: ```php \ae\path\configure('root', '/some/absolute/path'); $absolute_path = \ae\path('relative/path/to/file.ext'); ``` A part of your application may need to resolve a path relative to its own directory. In this case, instead of changing the configuration back and forth (which is very error prone), you should save the path to that directory to a variable: ```php $dir = \ae\path('some/dir'); $file = $dir->path('filename.ext'); // same as \ae\path('some/dir/filename.ext'); ``` `\ae\path()` function and `path()` method always return an object. You must explicitly cast it to string, when you need one: ```php $path_string = (string) $path_object; ``` When you cast (implicitly or explicitly) a path object to a string, the component will throw an `\ae\path\Exception`, if the path does not exist. If such behavior is undesirable, you should use `exists()`, `is_directory()`, and `is_file()` methods first to check, whether the path exists, and points to a directory or file. You can iterate path segments and access them individually using an index: ```php $path = \ae\path('some/file/path.txt'); $absolute_path = ''; foreach ($path as $segment) { $absolute_path.= '/' . $segment; } echo $absolute_path; $path[-3]; // 'some' $path[-2]; // 'file' $path[-1]; // 'path.txt' ``` ## File File component uses standard file functions: `fopen()`, `fclose()`, `fread()`, `fwrite()`, `copy()`, `rename()`, `is_uploaded_file()`, `move_uploaded_file()`, etc. All methods throw `\ae\file\Exception` on error. - Open and lock the file, and read and write its content: ```php $file = \ae\file('path/to/file.txt', \ae\file\writable & \ae\file\locked) ->truncate() ->append('Hello', ' ') ->append('World'); $file->content(); // 'Hello World' // Unlock file and close its handle unset($file); ``` - Access basic information about the file: ```php $file = \ae\file('path/to/file.txt'); $file->size(); // 12 $file->mime(); // 'text/plain' $file->name(); // 'file.txt' $file->extension(); // 'txt' $file->path(); // \ae\path('path/to/file.txt') ``` - Copy, move, and delete existing files: ```php $file = \ae\file('path/to/file.txt'); if ($file->exists()) { $copy = $file->copy('./file-copy.txt'); $file->delete(); $copy->move('./file.txt'); } ``` - Handle uploaded files: ```php $file = \ae\file($_FILES['file']['tmp_name']); if ($file->is_uploaded()) { $file->moveTo('path/to/uploads/')->rename($_FILES['file']['name']); } ``` - Assign arbitrary metadata to a file, e.g. database keys, related files, alternative names, etc.: ```php $file['real_name'] = 'My text file (1).txt'; $file['resource_id'] = 123; foreach ($file as $meta_name => $meta_value) { echo "{$meta_name}: $meta_value\n"; } ``` > **N.B.** Metadata is transient and is never saved to disk, but it may be used by different parts of your application to communicate additional information about the file. - Keep file size calculations readable: ```php \ae\file\byte; // 1 \ae\file\kilobyte; // 1000 \ae\file\kibibyte; // 1024 \ae\file\megabyte; // 1000000 \ae\file\mebibyte; // 1048576 \ae\file\gigabyte; // 1000000000 \ae\file\gibibyte; // 1073741824 \ae\file\terabyte; // 1000000000000 \ae\file\tebibyte; // 1099511627776 \ae\file\petabyte; // 1000000000000000 \ae\file\pebibyte; // 1125899906842624 ``` ## Image Image component is a wrapper around standard GD library functions. It lets you effortlessly resize, crop and apply filters to an image, and also: - Retrieve basic information about the image: ```php $image = \ae\image('example/image_320x240.jpg'); // image-specific info $image->width(); // 320 $image->height(); // 240 // general file info $image->size(); // 1024 $image->mime(); // 'image/jpeg' $image->name(); // 'image_320x240.jpg' $image->extension(); // 'jpg' ``` - Copy, move, and delete the image file: ```php $image = \ae\image('path/to/image.jpg'); if ($image->exists()) { $copy = $image->copy('./image-copy.jpg'); $image->delete(); $copy->move('./image.jpg'); } ``` ### Resizing and cropping You can transform the image by changing its dimensions in 4 different ways: - `scale($width, $height)` scales the image in one or both dimensions; pass `null` for either dimension to scale proportionally. - `crop($width, $height)` crops the image to specified dimensions; if the image is smaller in either dimension, the difference will be padded with transparent pixels. - `fit($width, $height)` scales the image, so it fits into a rectangle defined by target dimensions. - `fill($width, $height)` scales and crops the image, so it completely covers a rectangle defined by target dimensions. You will rarely need the first two methods, as the latter two cover most of use cases: ```php $photo = \ae\image('path/to/photo.jpg'); $regular = $photo ->fit(1600, 1600) ->suffix('_big') ->save(); $thumbnail = $photo ->fill(320, 320) ->suffix('_thumbnail') ->save(); ``` You can specify the point of origin using `align()` method, before you apply `crop()` and `fill()` transformations to an image: ```php $photo ->align(\ae\image\left, \ae\image\top) ->fill(320, 320) ->suffix('_thumbnail') ->save(); ``` This way you can crop a specific region of the image. The `align()` method requires two arguments: 1. Horizontal alignment: - a number from 0 (left) to 1 (right); 0.5 being the center - a constant: `\ae\image\left`, `\ae\image\center`, or `\ae\image\right` 2. Vertical alignment: - a number from 0 (top) to 1 (bottom); 0.5 being the middle - a constant: `\ae\image\top`, `\ae\image\middle`, or `\ae\image\bottom` By default, the origin point is in the middle of both axes. ### Applying filters You can also apply one or more filters: ```php $thumbnail ->blur() ->colorize(0.75, 0, 0) ->save(); ``` Here are all the filters available: - `blur()` blurs the image using the Gaussian method. - `brightness($value)` changes the brightness of the image; accepts a number from -1.0 to 1.0. - `contast($value)` changes the contrast of the image; accepts a number from -1.0 to 1.0. - `colorize($red, $green, $blue)` changes the average color of the image; accepts numbers from 0.0 to 1.0. - `grayscale()` converts the image into grayscale. - `negate()` reverses all colors of the image. - `pixelate($size)` applies pixelation effect to the image. - `smooth($value)` makes the image smoother; accepts integer values, -8 to 8 being the sweat spot. ### Conversion and saving By default when you use `save()` method the image type and name is preserved. If you want to preserve association with the original image, you can append or prepend a string to its name using `siffix()` and `prefix()` methods respectively: ```php \ae\image('some/photo.jpg') ->prefix('unknown_') ->suffix('graphic_image') ->save(); // image is saved as 'some/unknown_photographic_image.jpg' ``` If you want to change the name *or* image type completely, you should provide file name to `save()` method: ```php $image = \ae\image('some/image.png'); $image ->quality(75) ->progressive(true) ->save('image.jpg'); // image is saved as 'some/image.jpg' $image ->interlaced(true) ->save('image.gif'); // image is saved as 'some/image.gif' ``` ## Form Form component lets you create web forms and validate them both on the client and the server sides using HTML5 constraints. Both forms and individual controls render themselves into valid HTML when cast to string (by implementing `__toString()` magic method), but you can render them manually, if you so desire. ### Declaration You can create a new form using `\ae\form()` function. It takes form name as the first argument, and (optionally) an array of initial values as the second argument: ```php $form = \ae\form('Profile', [ 'name' => 'John Connor', ]); ``` > **N.B.** The form name must be unique within the context of a single web page. Once form is created, you can assign individual form fields to it: ```php $form['name'] = \ae\form\text('Full name') ->required() ->max_length(100); $form['email'] = \ae\form\email('Email address') ->required(); $form['phone'] = \ae\form\tel('Phone number'); // N.B. Never use \ae\form\number() for phone numbers! $form['phone_type'] = \ae\form\radio('Is it home, work, or mobile number?', 'home') ->options([ 'home' => 'Home number', 'work' => 'Work number', 'mobile' => 'Mobile number', ]) ->required(); $form['birth_date'] = \ae\form\date('Date of birth') // compare to date 18 years ago using strtotime() ->max('-18 years', 'You must be at least 18 years old!'); $form['photos'] = \ae\form\image('Photos of you') ->multiple() ->min_width(200) ->min_height(200) ->max_size(10 * \ae\file\megabyte); ``` > You can assign an instance of any subclass of `\ae\form\DataField` or `\ae\form\FileField` classes to a form. Once field is assigned, the form will use either `$_POST` or `$_FILES` array as its data source, depending on which parent class the object is related to. Field objects expose methods that you can use to define their validation constraints. Most of those constraints behave similar to their HTML5 counterparts. See [Constraints](#validation-constraints) section for more information. #### Basic field types Form component supports most kinds of ``, `
本源码包内暂不包含可直接显示的源代码文件,请下载源码包。