lithium\net\http\Media
The Media
class facilitates content-type mapping (mapping between content-types and file
extensions), handling static assets and globally configuring how the framework handles output in
different formats.
Using the Media
class, you can globally configure input and output of different types of
content, i.e.:
You may then render CSV content from anywhere in your application. For example, in a controller
you may do the following:
```
$this->render(['csv' => Post::find('all')]);
```
Source
class Media extends \lithium\core\StaticObjectDeprecated {
/**
* Contain the configuration of scopes.
*
* @var array of scopes
*/
protected static $_scopes = null;
/**
* Stores the name of the scope in use.
* If set to `false`, no scope is used.
*
* @see lithium\net\http\Media::scope()
* @var string
*/
protected static $_scope = false;
/**
* Maps file extensions to content-types. Used to set response types and determine request
* types. Can be modified with `Media::type()`.
*
* @var array
* @see lithium\net\http\Media::type()
*/
protected static $_types = [];
/**
* A map of media handler objects or callbacks, mapped to media types.
*
* @var array
*/
protected static $_handlers = [];
/**
* Contains default path settings for various asset types.
*
* For each type, the corresponding array key maps to the general type name, i.e. `'js'` or
* `'image'`. Each type contains a set of keys which define their locations and default
* behavior. For more information how each key works, see `Media::assets()`.
*
* @var array
* @see lithium\net\http\Media::assets()
*/
protected static $_assets = [];
/**
* Placeholder for class dependencies. This class' dependencies (i.e. templating classes) are
* typically specified through other configuration.
*
* @var array
*/
protected static $_classes = [
'configuration' => 'lithium\core\Configuration'
];
/**
* Returns the list of registered media types. New types can be set with the `type()` method.
*
* @return array Returns an array of media type extensions or short-names, which comprise the
* list of types handled.
*/
public static function types() {
return array_keys(static::_types());
}
/**
* Alias for `types()`; included for interface compatibility with
* `lithium\util\Collection::to()`, which allows a collection object to be exported to any
* format supported by a `Media` handler. See the documentation for `Collection::to()` for more
* information.
*
* @see lithium\net\http\Media
* @return array Returns the value of `Media::types()`.
*/
public static function formats() {
return static::types();
}
/**
* Alias for `encode()`; included for interface compatibility with
* `lithium\util\Collection::to()`, which allows a collection object to be exported to any
* format supported by a `Media` handler. See the documentation for `Collection::to()` for more
* information.
*
* @param mixed $format Format into which data will be converted, i.e. `'json'`.
* @param mixed $data Either an array or object (usually an instance of `Collection`) which will
* be converted into the specified format.
* @param array $options Additional handler-specific options to pass to the content handler.
* @return mixed
*/
public static function to($format, $data, array $options = []) {
return static::encode($format, $data, $options);
}
/**
* Maps a type name to a particular content-type (or multiple types) with a set of options, or
* retrieves information about a type that has been defined.
*
* Examples:
* ``` embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(1-2) ```
*
* ``` embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(19-23) ```
*
* ``` embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(43-44) ```
*
* Alternatively, can be used to detect the type name of a registered content type:
* ```
* Media::type('application/json'); // returns 'json'
* Media::type('application/javascript'); // returns 'javascript'
* Media::type('text/javascript'); // also returns 'javascript'
*
* Media::type('text/html'); // returns 'html'
* Media::type('application/xhtml+xml'); // also returns 'html'
* ```
*
* #### Content negotiation
*
* When creating custom media types, specifying which content-type(s) to match isn't always
* enough. For example, if you wish to serve a different set of templates to mobile web
* browsers, you'd still want those templates served as HTML. You might add something like this:
*
* ```
* Media::type('mobile', ['application/xhtml+xml', 'text/html']);
* ```
*
* However, this would cause _all_ requests for HTML content to be interpreted as
* `'mobile'`-type requests. Instead, we can use _content negotiation_ to granularly specify how
* to match a particular type. Content negotiation is the process of examining the HTTP headers
* provided in the request (including the content-types listed in the `Accept` header, and
* optionally other things as well, like the `Accept-Language` or `User-Agent` headers), in
* order to produce the best representation of the requested resource for the client; in other
* words, the resource that most closely matches what the client is asking for.
*
* Content negotiation with media types is made possible through the `'conditions'` key of the
* `$options` parameter, which contains an array of assertions made against the `Request`
* object. Each assertion (array key) can be one of three different things:
*
* - `'type'` _boolean_: In the default routing, some routes have `{:type}` keys, which are
* designed to match file extensions in URLs. These values act as overrides for the
* HTTP `Accept` header, allowing different formats to be served with the same content
* type. For example, if you're serving JSONP, you'll want to serve it with the same
* content-type as JavaScript (since it is JavaScript), but you probably won't want to
* use the same template(s) or other settings. Therefore, when serving JSONP content, you
* can specify that the extension defined in the type must be present in the URL:
* ```
* Media::type('jsonp', ['application/json'], [
* // template settings...
* 'conditions' => ['type' => true]
* ]);
* ```
* Then, JSONP content will only ever be served when the request URL ends in `.jsonp`.
*
* - `'<prefix>:<key>'` _string_: This type of assertion can be used to match against arbitrary
* information in the request, including headers (i.e. `'http:user_agent'`), environment
* variables (i.e. `'env:home'`), GET and POST data (i.e. `'query:foo'` or `'data:foo'`,
* respectively), and the HTTP method (`'http:method'`) of the request. For more information
* on possible keys, see `lithium\action\Request::get()`.
*
* - `'<detector>'` _boolean_: Uses detector checks added to the `Request` object to make
* boolean assertions against the request. For example, if a detector called `'iPhone'` is
* attached, you can add `'iPhone' => true` to the `'conditions'` array in order to filter for
* iPhone requests only. See `lithium\action\Request::detect()` for more information on adding
* detectors.
*
* @link http://en.wikipedia.org/wiki/JSON#JSONP
* @see lithium\net\http\Media::$_types
* @see lithium\net\http\Media::$_handlers
* @see lithium\net\http\Media::negotiate()
* @see lithium\action\Request::get()
* @see lithium\action\Request::is()
* @see lithium\action\Request::detect()
* @see lithium\util\Text::insert()
* @param string $type A file-extension-style type name, i.e. `'txt'`, `'js'`, or `'atom'`.
* Alternatively, a mapped content type, i.e. `'text/html'`,
* `'application/atom+xml'`, etc.; in which case, the matching type name (i.e.
* '`html'` or `'atom'`) will be returned.
* @param mixed $content Optional. A string or array containing the content-type(s) that
* `$type` should map to. If `$type` is an array of content-types, the first one listed
* should be the "primary" type, and will be used as the `Content-type` header of any
* `Response` objects served through this type.
* @param array $options Optional. The handling options for this media type. Possible keys are:
* - `'view'` _string_: Specifies the view class to use when rendering this content.
* Note that no `'view'` class is specified by default. If you want to
* render templates using Lithium's default view class, use
* `'lithium\template\View'`
* - `'decode'` _mixed_: A (string) function name or (object) closure that handles
* decoding or unserializing content from this format.
* - `'encode'` _mixed_: A (string) function name or (object) closure that handles
* encoding or serializing content into this format.
* - `'cast'` _boolean_: Used with `'encode'`. If `true`, all data passed into the
* specified encode function is first cast to array structures.
* - `'paths'` _array_: Optional key/value pairs mapping paths for
* `'template'`, `'layout'`, and `'element'` template files. Any keys ommitted
* will use the default path. The values should be `Text::insert()`-style
* paths or an array of `Text::insert()`-style paths. If it is an array,
* each path will be tried in the order specified until a template is found.
* This is useful for allowing custom templates while falling back on
* default templates if no custom template was found. If you want to
* render templates without a layout, use a `false` value for `'layout'`.
* - `'conditions'` _array_: Optional key/value pairs used as assertions in content
* negotiation. See the above section on **Content Negotiation**.
* @return mixed If `$content` and `$options` are empty, returns an array with `'content'` and
* `'options'` keys, where `'content'` is the content-type(s) that correspond to
* `$type` (can be a string or array, if multiple content-types are available), and
* `'options'` is the array of options which define how this content-type should be
* handled. If `$content` or `$options` are non-empty, returns `null`.
*/
public static function type($type, $content = null, array $options = []) {
$defaults = [
'view' => false,
'paths' => [
'template' => '{:library}/views/{:controller}/{:template}.{:type}.php',
'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
'element' => '{:library}/views/elements/{:template}.{:type}.php'
],
'encode' => false,
'decode' => false,
'cast' => true,
'conditions' => []
];
if ($content === false) {
unset(static::$_types[$type], static::$_handlers[$type]);
}
if (!$content && !$options) {
if (!$content = static::_types($type)) {
return;
}
if (strpos($type, '/')) {
return $content;
}
if (is_array($content) && isset($content['alias'])) {
return static::type($content['alias']);
}
return compact('content') + ['options' => static::handlers($type)];
}
if ($content) {
static::$_types[$type] = (array) $content;
}
static::$_handlers[$type] = $options ? Set::merge($defaults, $options) : [];
}
/**
* Performs content-type negotiation on a `Request` object, by iterating over the accepted
* types in sequence, from most preferred to least, and attempting to match each one against a
* content type defined by `Media::type()`, until a match is found. If more than one defined
* type matches for a given content type, they will be checked in the order they were added
* (usually, this corresponds to the order they were defined in the application bootstrapping
* process).
*
* @see lithium\net\http\Media::type()
* @see lithium\net\http\Media::match()
* @see lithium\action\Request
* @param \lithium\action\Request $request The request which contains the details of
* the request to be content-negotiated.
* @return string|null Returns the first matching type name, i.e. `'html'` or `'json'`. When
* no matching type is found returns `null`.
*/
public static function negotiate($request) {
$match = function($name) use ($request) {
if (($cfg = static::type($name)) && static::match($request, compact('name') + $cfg)) {
return true;
}
return false;
};
if (($type = $request->type) && $match($type)) {
return $type;
}
foreach ($request->accepts(true) as $type) {
if (!$types = (array) static::_types($type)) {
continue;
}
foreach ($types as $name) {
if (!$match($name)) {
continue;
}
return $name;
}
}
}
/**
* Assists `Media::negotiate()` in processing the negotiation conditions of a content type, by
* iterating through the conditions and checking each one against the `Request` object.
*
* @see lithium\net\http\Media::negotiate()
* @see lithium\net\http\Media::type()
* @see lithium\action\Request
* @param \lithium\action\Request $request The request to be checked against a
* set of conditions (if applicable).
* @param array $config Represents a content type configuration, which is an array containing 3
* keys:
* - `'name'` _string_: The type name, i.e. `'html'` or `'json'`.
* - `'content'` _mixed_: One or more content types that the configuration
* represents, i.e. `'text/html'`, `'application/xhtml+xml'` or
* `'application/json'`, or an array containing multiple content types.
* - `'options'` _array_: An array containing rendering information, and an
* optional `'conditions'` key, which contains an array of matching parameters.
* For more details on these matching parameters, see `Media::type()`.
* @return boolean Returns `true` if the information in `$request` matches the type
* configuration in `$config`, otherwise false.
*/
public static function match($request, array $config) {
if (!isset($config['options']['conditions'])) {
return true;
}
$conditions = $config['options']['conditions'];
foreach ($conditions as $key => $value) {
switch (true) {
case $key === 'type':
if ($value !== ($request->type === $config['name'])) {
return false;
}
break;
case strpos($key, ':'):
if ($request->get($key) !== $value) {
return false;
}
break;
case ($request->is($key) !== $value):
return false;
break;
}
}
return true;
}
/**
* Gets or sets options for various asset types.
*
* @see lithium\util\Text::insert()
* @param string $type The name of the asset type, i.e. `'js'` or `'css'`.
* @param array $options If registering a new asset type or modifying an existing asset type,
* contains settings for the asset type, where the available keys are as follows:
* - `'suffix'`: The standard suffix for this content type, with leading dot ('.') if
* applicable.
* - `'filter'`: An array of key/value pairs representing simple string replacements to
* be done on a path once it is generated.
* - `'paths'`: An array of key/value pairs where the keys are `Text::insert()`
* compatible paths, and the values are array lists of keys to be inserted into the
* path string.
* @return array If `$type` is empty, an associative array of all registered types and all
* associated options is returned. If `$type` is a string and `$options` is empty,
* returns an associative array with the options for `$type`. If `$type` and `$options`
* are both non-empty, returns `null`.
*/
public static function assets($type = null, $options = []) {
$defaults = ['suffix' => null, 'filter' => null, 'paths' => []];
if (!$type) {
return static::_assets();
}
if ($options === false) {
unset(static::$_assets[$type]);
}
if (!$options) {
return static::_assets($type);
}
$options = (array) $options + $defaults;
if ($base = static::_assets($type)) {
$options = array_merge($base, array_filter($options));
}
static::$_assets[$type] = $options;
}
/**
* Calculates the web-accessible path to a static asset, usually a JavaScript, CSS or image
* file.
*
* @see lithium\net\http\Media::$_assets
* @see lithium\action\Request::env()
* @param string $path The path to the asset, relative to the given `$type`s path and without a
* suffix. If the path contains a URI Scheme (eg. `http://`), no path munging will occur.
* @param string $type The asset type. See `Media::$_assets` or `Media::assets()`.
* @param array $options Contains setting for finding and handling the path, where the keys are
* the following:
* - `'base'`: The base URL of your application. Defaults to `null` for no base path.
* This is usually set with the return value of a call to `env('base')` on an instance
* of `lithium\action\Request`.
* - `'check'`: Check for the existence of the file before returning. Defaults to
* `false`.
* - `'filter'`: An array of key/value pairs representing simple string replacements to
* be done on a path once it is generated.
* - `'paths'`: An array of paths to search for the asset in. The paths should use
* `Text::insert()` formatting. See `Media::$_assets` for more.
* - `suffix`: The suffix to attach to the path, generally a file extension.
* - `'timestamp'`: Appends the last modified time of the file to the path if `true`.
* Defaults to `false`.
* - `'library'`: The name of the library from which to load the asset. Defaults to
* `true`, for the default library.
* @return string Returns the publicly-accessible absolute path to the static asset. If checking
* for the asset's existence (`$options['check']`), returns `false` if it does not exist
* in your `/webroot` directory, or the `/webroot` directories of one of your included
* plugins.
* @filter
*/
public static function asset($path, $type, array $options = []) {
$options = static::_assetOptions($path, $type, $options);
$params = compact('path', 'type', 'options');
return Filters::run(get_called_class(), __FUNCTION__, $params, function($params) {
$path = $params['path'];
$type = $params['type'];
$options = $params['options'];
$library = $options['library'];
if (preg_match('/^(?:[a-z0-9-]+:)?\/\//i', $path)) {
return $path;
}
$config = Libraries::get($library);
$paths = $options['paths'];
$config['default'] ? end($paths) : reset($paths);
$options['library'] = basename($config['path']);
if ($options['suffix'] && strpos($path, $options['suffix']) === false) {
$path .= $options['suffix'];
}
return static::filterAssetPath($path, $paths, $config, compact('type') + $options);
});
}
/**
* Initialize options for `Media::asset()`.
*
* @param string $path The path to the asset.
* @param string $type The asset type.
* @param array $options Contains setting for finding and handling the path.
* @return array The initialized options.
*/
protected static function _assetOptions($path, $type, $options) {
$defaults = [
'base' => null,
'timestamp' => false,
'filter' => null,
'paths' => [],
'suffix' => null,
'check' => false,
'library' => true
];
$base = isset($options['base']) ? rtrim($options['base'], '/') : '';
$options += ['scope' => static::scope()];
$name = $options['scope'];
if ($config = static::attached($name)) {
$base = isset($config['base']) ? '/' . $config['base'] : $base;
$defaults = array_merge($defaults, $config);
if (preg_match('/^((?:[a-z0-9-]+:)?\/\/)([^\/]*)/i', $base, $match)) {
$options = array_merge($defaults, [
'base' => rtrim($base . '/' . $defaults['prefix'], '/')
]);
} else {
$host = '';
if ($defaults['absolute']) {
$host = $defaults['host'];
if (is_array($host)) {
$hash = substr(hexdec(md5($path)), 0, 10);
$index = ((integer) $hash) % count($host);
if (is_array($defaults['scheme'])) {
$host = $defaults['scheme'][$index] . $host[$index];
} else {
$host = $defaults['scheme'] . $host[$index];
}
} else {
$host = $defaults['scheme'] . $defaults['host'];
}
$base = trim($base, '/');
$base = $base ? '/' . $base : '';
}
$options['base'] = rtrim($host . $base . '/' . $defaults['prefix'], '/');
}
}
if (!$paths = static::_assets($type)) {
$paths = static::_assets('generic');
}
return $options + $paths + $defaults;
}
/**
* Performs checks and applies transformations to asset paths, including verifying that the
* virtual path exists on the filesystem, appending a timestamp, prepending an asset host, or
* applying a user-defined filter.
*
* @see lithium\net\http\Media::asset()
* @param string $asset A full asset path, relative to the public web path of the application.
* @param mixed $path Path information for the asset type.
* @param array $config The configuration array of the library from which the asset is being
* loaded.
* @param array $options The array of options passed to `asset()` (see the `$options` parameter
* of `Media::asset()`).
* @return mixed Returns a modified path to a web asset, or `false`, if the path fails a check.
*/
public static function filterAssetPath($asset, $path, array $config, array $options = []) {
$config += ['assets' => null];
if ($options['check'] || $options['timestamp']) {
$file = static::path($asset, $options['type'], $options);
}
if ($options['check'] && !is_file($file)) {
return false;
}
$isAbsolute = ($asset && $asset[0] === '/');
if ($isAbsolute && $options['base'] && strpos($asset, $options['base']) !== 0) {
$asset = "{$options['base']}{$asset}";
} elseif (!$isAbsolute) {
$asset = Text::insert(key($path), ['path' => $asset] + $options);
}
if (is_array($options['filter']) && !empty($options['filter'])) {
$filter = $options['filter'];
$asset = str_replace(array_keys($filter), array_values($filter), $asset);
}
if ($options['timestamp'] && is_file($file)) {
$separator = (strpos($asset, '?') !== false) ? '&' : '?';
$asset .= $separator . filemtime($file);
}
if ($host = $config['assets']) {
$type = $options['type'];
$env = Environment::get();
$base = isset($host[$env][$type]) ? $host[$env][$type] : null;
$base = (isset($host[$type]) && !$base) ? $host[$type] : $base;
if ($base) {
return "{$base}{$asset}";
}
}
return $asset;
}
/**
* Gets the physical path to the web assets (i.e. `/webroot`) directory of a library.
*
* @param string|boolean $library The name of the library for which to find the path, or `true`
* for the default library.
* @param string|boolean $scope The name of the to use to find the path.
* @return string Returns the physical path to the web assets directory.
*/
public static function webroot($library = true, $scope = null) {
if ($scope) {
if ($config = static::attached($scope)) {
return $config['path'];
}
return null;
}
if (!$config = Libraries::get($library)) {
return null;
}
if (isset($config['webroot'])) {
return $config['webroot'];
}
if (isset($config['path'])) {
return $config['path'] . '/webroot';
}
}
/**
* Returns the physical path to an asset in the `/webroot` directory of an application or
* plugin.
*
* @param string $paths The path to a web asset, relative to the root path for its type. For
* example, for a JavaScript file in `/webroot/js/subpath/file.js`, the correct
* value for `$path` would be `'subpath/file.js'`.
* @param string $type A valid asset type, i.e. `'js'`, `'cs'`, `'image'`, or another type
* registered with `Media::assets()`, or `'generic'`.
* @param array $options The options used to calculate the path to the file.
* @return string Returns the physical filesystem path to an asset in the `/webroot` directory.
*/
public static function path($path, $type, array $options = []) {
$defaults = [
'base' => null,
'paths' => [],
'suffix' => null,
'library' => true,
'scope' => false
];
if (!$base = static::_assets($type)) {
$base = static::_assets('generic');
}
$options += ($base + $defaults);
$paths = $options['paths'];
if ($options['scope']) {
$root = static::webroot(false, $options['scope']);
} else {
$root = static::webroot($options['library']);
Libraries::get(true, 'name') === $options['library'] ? end($paths) : reset($paths);
}
if ($qOffset = strpos($path, '?')) {
$path = substr($path, 0, $qOffset);
}
if ($path[0] === '/') {
$file = $root . $path;
} else {
$template = str_replace('{:library}/', '', key($paths));
$insert = ['base' => $root] + compact('path');
$file = Text::insert($template, $insert);
}
return realpath($file);
}
/**
* Renders data (usually the result of a controller action) and generates a string
* representation of it, based on the type of expected output.
*
* @param object $response A Response object into which the operation will be
* rendered. The content of the render operation will be assigned to the `$body`
* property of the object, the `'Content-Type'` header will be set accordingly, and it
* will be returned.
* @param mixed $data The data (usually an associative array) to be rendered in the response.
* @param array $options Any options specific to the response being rendered, such as type
* information, keys (i.e. controller and action) used to generate template paths,
* etc.
* @return object Returns a modified `Response` object with headers and body defined.
* @filter
*/
public static function render($response, $data = null, array $options = []) {
$params = compact('response', 'data', 'options');
return Filters::run(get_called_class(), __FUNCTION__, $params, function($params) {
$types = static::_types();
$handlers = static::handlers();
$defaults = ['encode' => null, 'template' => null, 'layout' => '', 'view' => null];
$response = $params['response'];
$data = $params['data'];
$options = $params['options'] + ['type' => $response->type()];
$result = null;
$type = $options['type'];
if (!isset($handlers[$type])) {
throw new MediaException("Unhandled media type `{$type}`.");
}
$handler = $options + $handlers[$type] + $defaults;
$filter = function($v) { return $v !== null; };
$handler = array_filter($handler, $filter) + $handlers['default'] + $defaults;
if (isset($types[$type])) {
$mimeTypes = (array) $types[$type];
$header = current($mimeTypes);
$header .= $response->encoding ? "; charset={$response->encoding}" : '';
$response->headers('Content-Type', $header);
}
$response->body(static::_handle($handler, $data, $response));
return $response;
});
}
/**
* Configures a template object instance, based on a media handler configuration.
*
* @see lithium\net\http\Media::type()
* @see lithium\template\View::render()
* @see lithium\action\Response
* @param mixed $handler Either a string specifying the name of a media type for which a handler
* is defined, or an array representing a handler configuration. For more on types
* and type handlers, see the `type()` method.
* @param mixed $data The data to be rendered. Usually an array.
* @param object $response The `Response` object associated with this dispatch cycle. Usually an
* instance of `lithium\action\Response`.
* @param array $options Any options that will be passed to the `render()` method of the
* templating object.
* @return object Returns an instance of a templating object, usually `lithium\template\View`.
* @filter
*/
public static function view($handler, $data, &$response = null, array $options = []) {
$params = ['response' => &$response] + compact('handler', 'data', 'options');
return Filters::run(get_called_class(), __FUNCTION__, $params, function($params) {
$handler = $params['handler'];
$response =& $params['response'];
$handler = is_array($handler) ? $handler : static::handlers($handler);
$class = $handler['view'];
unset($handler['view']);
$config = $handler + ['response' => &$response];
return Libraries::instance(null, $class, $config, static::$_classes);
});
}
/**
* For media types registered in `$_handlers` which include an `'encode'` setting, encodes data
* according to the specified media type.
*
* @see lithium\net\http\Media::type()
* @param mixed $handler Specifies the media type into which `$data` will be encoded. This media
* type must have an `'encode'` setting specified in `Media::$_handlers`.
* Alternatively, `$type` can be an array, in which case it is used as the type
* handler configuration. See the `type()` method for information on adding type
* handlers, and the available configuration keys.
* @param mixed $data Arbitrary data you wish to encode. Note that some encoders can only handle
* arrays or objects.
* @param object $response A reference to the `Response` object for this dispatch cycle.
* @return mixed Returns the result of `$data`, encoded with the encoding configuration
* specified by `$type`, the result of which is usually a string.
* @filter
*/
public static function encode($handler, $data, &$response = null) {
$params = ['response' => &$response] + compact('handler', 'data');
return Filters::run(get_called_class(), __FUNCTION__, $params, function($params) {
$data = $params['data'];
$handler = $params['handler'];
$response =& $params['response'];
$handler = is_array($handler) ? $handler : static::handlers($handler);
if (!$handler || empty($handler['encode'])) {
return null;
}
$cast = function($data) {
if (!is_object($data)) {
return $data;
}
return method_exists($data, 'to') ? $data->to('array') : get_object_vars($data);
};
if (!isset($handler['cast']) || $handler['cast']) {
$data = is_object($data) ? $cast($data) : $data;
$data = is_array($data) ? array_map($cast, $data) : $data;
}
$method = $handler['encode'];
return is_string($method) ? $method($data) : $method($data, $handler, $response);
});
}
/**
* For media types registered in `$_handlers` which include an `'decode'` setting, decodes data
* according to the specified media type.
*
* @param string $type Specifies the media type into which `$data` will be encoded. This media
* type must have an `'encode'` setting specified in `Media::$_handlers`.
* @param mixed $data Arbitrary data you wish to encode. Note that some encoders can only handle
* arrays or objects.
* @param array $options Handler-specific options.
* @return mixed
*/
public static function decode($type, $data, array $options = []) {
if ((!$handler = static::handlers($type)) || empty($handler['decode'])) {
return null;
}
$method = $handler['decode'];
return is_string($method) ? $method($data) : $method($data, $handler + $options);
}
/**
* Resets the `Media` class to its default state. Mainly used for ensuring a consistent state
* during testing.
*/
public static function reset() {
static::$_handlers = [];
static::$_types = [];
static::$_scope = false;
if (isset(static::$_scopes)) {
static::$_scopes->reset();
}
}
/**
* Called by `Media::render()` to render response content. Given a content handler and data,
* calls the content handler and passes in the data, receiving back a rendered content string.
*
* @see lithium\action\Response
* @param array $handler
* @param array $data
* @param object $response A reference to the `Response` object for this dispatch cycle.
* @return string
* @filter
*/
protected static function _handle($handler, $data, &$response) {
$params = ['response' => &$response] + compact('handler', 'data');
return Filters::run(get_called_class(), __FUNCTION__, $params, function($params) {
$response = $params['response'];
$handler = $params['handler'];
$data = $params['data'];
$options = $handler;
if (isset($options['request'])) {
$options += $options['request']->params;
unset($options['request']);
}
switch (true) {
case $handler['encode']:
return static::encode($handler, $data, $response);
case ($handler['template'] === false) && is_string($data):
return $data;
case $handler['view']:
unset($options['view']);
$instance = static::view($handler, $data, $response, $options);
return $instance->render('all', (array) $data, $options);
default:
throw new MediaException("Could not interpret type settings for handler.");
}
});
}
/**
* Helper method for listing registered media types. Returns all types, or a single
* content type if a specific type is specified.
*
* @todo Use fnmatch() to support wildcards.
* @param string $type Type to return.
* @return mixed Array of types, or single type requested.
*/
protected static function _types($type = null) {
$types = static::$_types + [
'html' => ['text/html', 'application/xhtml+xml', '*/*'],
'htm' => ['alias' => 'html'],
'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'],
'json' => ['application/json'],
'rss' => ['application/rss+xml'],
'atom' => ['application/atom+xml'],
'css' => ['text/css'],
'js' => ['application/javascript', 'text/javascript'],
'text' => ['text/plain'],
'txt' => ['alias' => 'text'],
'xml' => ['application/xml', 'application/soap+xml', 'text/xml']
];
if (!$type) {
return $types;
}
if (strpos($type, '/') === false) {
return isset($types[$type]) ? $types[$type] : null;
}
if (strpos($type, ';')) {
list($type) = explode(';', $type, 2);
}
$result = [];
foreach ($types as $name => $cTypes) {
if ($type === $cTypes || (is_array($cTypes) && in_array($type, $cTypes))) {
$result[] = $name;
}
}
if (count($result) === 1) {
return reset($result);
}
return $result ?: null;
}
/**
* Helper method for listing registered type handlers. Returns all handlers, or the
* handler for a specific media type, if requested.
*
* @param string $type The type of handler to return.
* @return mixed Array of all handlers, or the handler for a specific type.
*/
public static function handlers($type = null) {
$handlers = static::$_handlers + [
'default' => [
'view' => 'lithium\template\View',
'encode' => false,
'decode' => false,
'cast' => false,
'paths' => [
'template' => '{:library}/views/{:controller}/{:template}.{:type}.php',
'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
'element' => '{:library}/views/elements/{:template}.{:type}.php'
]
],
'html' => [],
'json' => [
'cast' => true,
'encode' => 'json_encode',
'decode' => function($data) {
return json_decode($data, true);
}
],
'text' => ['cast' => false, 'encode' => function($s) { return $s; }],
'form' => [
'cast' => true,
'encode' => 'http_build_query',
'decode' => function($data) {
$decoded = [];
parse_str($data, $decoded);
return $decoded;
}
]
];
if ($type) {
return isset($handlers[$type]) ? $handlers[$type] : null;
}
return $handlers;
}
/**
* Helper method to list all asset paths, or the path for a single type.
*
* @param string $type The type you wish to get paths for.
* @return mixed An array of all paths, or a single array of paths for the
* given type.
*/
protected static function _assets($type = null) {
$assets = static::$_assets + [
'js' => ['suffix' => '.js', 'filter' => null, 'paths' => [
'{:base}/{:library}/js/{:path}' => ['base', 'library', 'path'],
'{:base}/js/{:path}' => ['base', 'path']
]],
'css' => ['suffix' => '.css', 'filter' => null, 'paths' => [
'{:base}/{:library}/css/{:path}' => ['base', 'library', 'path'],
'{:base}/css/{:path}' => ['base', 'path']
]],
'image' => ['suffix' => null, 'filter' => null, 'paths' => [
'{:base}/{:library}/img/{:path}' => ['base', 'library', 'path'],
'{:base}/img/{:path}' => ['base', 'path']
]],
'generic' => ['suffix' => null, 'filter' => null, 'paths' => [
'{:base}/{:library}/{:path}' => ['base', 'library', 'path'],
'{:base}/{:path}' => ['base', 'path']
]]
];
if ($type) {
return isset($assets[$type]) ? $assets[$type] : null;
}
return $assets;
}
/**
* Scope getter/setter.
*
* Special use case: If `$closure` is not null executing the closure inside
* the specified scope.
*
* @param string $name Name of the scope to use.
* @param \Closure $closure A closure to execute inside the scope.
* @return mixed Returns the previous scope if if `$name` is not null and `$closure` is null,
* returns the default used scope if `$name` is null, otherwise returns `null`.
*/
public static function scope($name = null, Closure $closure = null) {
if ($name === null) {
return static::$_scope;
}
if ($closure === null) {
$former = static::$_scope;
static::$_scope = $name;
return $former;
}
$former = static::$_scope;
static::$_scope = $name;
call_user_func($closure);
static::$_scope = $former;
}
/**
* Attach a scope to a mount point.
*
* Example:
* ```
* Media::attach('app', [
* 'path' => '/var/www/website/webroot/extradir',
* 'prefix' => 'extradir'
* ]);
* ```
*
* ```
* Media::attach('cdn', [
* 'absolute' => true,
* 'path' => null,
* 'host' => 'http://my.cdn.com',
* 'prefix' => 'project1/assets'
* ]);
* ```
*
* ```
* Media::attach('cdn', [
* 'absolute' => true,
* 'path' => null,
* 'host' => ['my.cdn.com', 'secure.cdn.com'],
* 'scheme' => ['http://', 'https://'],
* 'prefix' => 'project1/assets',
* ]);
* ```
*
* ```
* Media::attach('cdn', [
* 'absolute' => true,
* 'path' => null,
* 'host' => ['my1.cdn.com', 'my2.cdn.com'],
* 'scheme' => 'http://',
* 'prefix' => 'project1/assets',
* ]);
* ```
*
* @param string $name The name of the media you wish to attach.
* @param array $config Asset configuration options for the given scope.
* - `'path'` _string_: Path of the media.
* - `'prefix'` _string_: Contains the uri prefix. Such as `css`.
* - `'absolute'` _boolean_: Defaults to `false`. If you want to generate
* absolute URL's.
* - `'host'` _mixed_: String host, or array of hosts, of the media, if absolute is
* `true`.
* - `'scheme'` _mixed_: String scheme, or array of sc, of the media, if absolute is
* `true`.
* @return void
*/
public static function attach($name, $config = null) {
if (!isset(static::$_scopes)) {
static::_initScopes();
}
if ($name === false) {
$name = '__defaultScope__';
}
if (is_array($config) || $config === false) {
static::$_scopes->set($name, $config);
}
}
/**
* Returns an attached mount point configuration.
*/
public static function attached($name = null) {
if (!isset(static::$_scopes)) {
static::_initScopes();
}
if ($name === false) {
$name = '__defaultScope__';
}
return static::$_scopes->get($name);
}
/**
* Initialize `static::$_scopes` with a `lithium\core\Configuration` instance.
*/
protected static function _initScopes() {
static::$_scopes = Libraries::instance(null, 'configuration', [], static::$_classes);
static::$_scopes->initConfig = function($name, $config) {
$defaults = [
'absolute' => false,
'host' => 'localhost',
'scheme' => 'http://',
'base' => null,
'prefix' => '',
'path' => null,
'timestamp' => false,
'filter' => null,
'suffix' => null,
'check' => false
];
$config += $defaults;
$config['prefix'] = trim($config['prefix'], '/');
$config['base'] = $config['base'] ? trim($config['base'], '/') : $config['base'];
return $config;
};
}
}