Commit: 6a810fb562923d0b8dfb31babede98f9b256246f
Author: chawbacca | Date: 2011-04-20 18:10:55 +0000
diff --git a/.htaccess b/.htaccess
index 417c613..33e4f2e 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,5 +1,5 @@
<IfModule mod_rewrite.c>
- RewriteEngine on
- RewriteRule ^$ app/webroot/ [L]
- RewriteRule (.*) app/webroot/$1 [L]
+ RewriteEngine on
+ RewriteRule ^$ app/webroot/ [L]
+ RewriteRule (.*) app/webroot/$1 [L]
</IfModule>
diff --git a/app/config/bootstrap.php b/app/config/bootstrap.php
index 2a7eaaf..e708745 100644
--- a/app/config/bootstrap.php
+++ b/app/config/bootstrap.php
@@ -7,43 +7,47 @@
*/
/**
- * This is the path to the class libraries used by your application, and must contain a copy of the
- * Lithium core. By default, this directory is named 'libraries', and resides in the same
- * directory as your application. If you use the same libraries in multiple applications, you can
- * set this to a shared path on your server.
+ * This is the primary bootstrap file of your application, and is loaded immediately after the front
+ * controller (`webroot/index.php`) is invoked. It includes references to other feature-specific
+ * bootstrap files that you can turn on and off to configure the services needed for your
+ * application.
+ *
+ * Besides global configuration of external application resources, these files also include
+ * configuration for various classes to interact with one another, usually through _filters_.
+ * Filters are Lithium's system of creating interactions between classes without tight coupling. See
+ * the `Filters` class for more information.
+ *
+ * If you have other services that must be configured globally for the entire application, create a
+ * new bootstrap file and `require` it here.
+ *
+ * @see lithium\util\collection\Filters
*/
-define('LITHIUM_LIBRARY_PATH', dirname(dirname(__DIR__)) . '/libraries');
/**
- * This is the path to your application's directory. It contains all the sub-folders for your
- * application's classes and files. You don't need to change this unless your webroot folder is
- * stored outside of your app folder.
+ * The libraries file contains the loading instructions for all plugins, frameworks and other class
+ * libraries used in the application, including the Lithium core, and the application itself. These
+ * instructions include library names, paths to files, and any applicable class-loading rules. This
+ * file also statically loads common classes to improve bootstrap performance.
*/
-define('LITHIUM_APP_PATH', dirname(__DIR__));
+require __DIR__ . '/bootstrap/libraries.php';
/**
- * Locate and load Lithium core library files. Throws a fatal error if the core can't be found.
- * If your Lithium core directory is named something other than 'lithium', change the string below.
+ * The error configuration allows you to use the filter system along with the advanced matching
+ * rules of the `ErrorHandler` class to provide a high level of control over managing exceptions in
+ * your application, with no impact on framework or application code.
*/
-if (!include LITHIUM_LIBRARY_PATH . '/lithium/core/Libraries.php') {
- $message = "Lithium core could not be found. Check the value of LITHIUM_LIBRARY_PATH in ";
- $message .= "config/bootstrap.php. It should point to the directory containing your ";
- $message .= "/libraries directory.";
- trigger_error($message, E_USER_ERROR);
-}
+// require __DIR__ . '/bootstrap/errors.php';
/**
- * This file contains the loading instructions for all class libraries used in the application,
- * including the Lithium core, and the application itself. These instructions include library names,
- * paths to files, and any applicable class-loading rules. Also includes any statically-loaded
- * classes to improve bootstrap performance.
+ * This file contains configurations for connecting to external caching resources, as well as
+ * default caching rules for various systems within your application
*/
-require __DIR__ . '/bootstrap/libraries.php';
+require __DIR__ . '/bootstrap/cache.php';
/**
- * Include this file if your application uses a database connection.
+ * Include this file if your application uses one or more database connections.
*/
-// require __DIR__ . '/connections.php';
+require __DIR__ . '/bootstrap/connections.php';
/**
* This file defines bindings between classes which are triggered during the request cycle, and
@@ -53,10 +57,10 @@ require __DIR__ . '/bootstrap/libraries.php';
require __DIR__ . '/bootstrap/action.php';
/**
- * This file contains configurations for connecting to external caching resources, as well as
- * default caching rules for various systems within your application
+ * This file contains configuration for session (and/or cookie) storage, and user or web service
+ * authentication.
*/
-require __DIR__ . '/bootstrap/cache.php';
+// require __DIR__ . '/bootstrap/session.php';
/**
* This file contains your application's globalization rules, including inflections,
@@ -76,15 +80,5 @@ require __DIR__ . '/bootstrap/cache.php';
*/
// require __DIR__ . '/bootstrap/console.php';
-/**
- * This configures your session storage. The Cookie storage adapter must be connected first, since
- * it intercepts any writes where the `'expires'` key is set in the options array.
- */
-// use \lithium\storage\Session;
-//
-// Session::config(array(
-// 'cookie' => array('adapter' => 'Cookie', 'expire' => '+2 days'),
-// 'default' => array('adapter' => 'Php')
-// ));
?>
\ No newline at end of file
diff --git a/app/config/bootstrap/action.php b/app/config/bootstrap/action.php
index 6139e86..41b4d5e 100644
--- a/app/config/bootstrap/action.php
+++ b/app/config/bootstrap/action.php
@@ -17,39 +17,39 @@
* @see lithium\util\collection\Filters
*/
-use \lithium\core\Libraries;
-use \lithium\net\http\Router;
-use \lithium\core\Environment;
-use \lithium\action\Dispatcher;
+use lithium\core\Libraries;
+use lithium\net\http\Router;
+use lithium\core\Environment;
+use lithium\action\Dispatcher;
/**
- * This filter loads all application routes in all plugins, loading the default application routes
- * last. Change this code if plugin routes must be loaded in a specific order, or if application
- * routes must be loaded first (in which case the catch-all routes should be removed). If
- * `Dispatcher::run()` is called multiple times in the course of a single request, change the
+ * This filter intercepts the `run()` method of the `Dispatcher`, and first passes the `'request'`
+ * parameter (an instance of the `Request` object) to the `Environment` class to detect which
+ * environment the application is running in. Then, loads all application routes in all plugins,
+ * loading the default application routes last.
+ *
+ * Change this code if plugin routes must be loaded in a specific order (i.e. not the same order as
+ * the plugins are added in your bootstrap configuration), or if application routes must be loaded
+ * first (in which case the default catch-all routes should be removed).
+ *
+ * If `Dispatcher::run()` is called multiple times in the course of a single request, change the
* `include`s to `include_once`.
*
+ * @see lithium\action\Request
+ * @see lithium\core\Environment
* @see lithium\net\http\Router
*/
Dispatcher::applyFilter('run', function($self, $params, $chain) {
+ Environment::set($params['request']);
+
foreach (array_reverse(Libraries::get()) as $name => $config) {
- if ($name === 'lithium') continue;
+ if ($name === 'lithium') {
+ continue;
+ }
$file = "{$config['path']}/config/routes.php";
file_exists($file) ? include $file : null;
}
return $chain->next($self, $params, $chain);
});
-/**
- * Intercepts the `Dispatcher` as it finds a controller object, and passes the `'request'` parameter
- * to the `Environment` class to detect which environment the application is running in.
- *
- * @see lithium\action\Request
- * @see lithium\core\Environment
- */
-Dispatcher::applyFilter('_callable', function($self, $params, $chain) {
- Environment::set($params['request']);
- return $chain->next($self, $params, $chain);
-});
-
?>
\ No newline at end of file
diff --git a/app/config/bootstrap/cache.php b/app/config/bootstrap/cache.php
index 2039220..68f11e0 100644
--- a/app/config/bootstrap/cache.php
+++ b/app/config/bootstrap/cache.php
@@ -15,28 +15,46 @@ use lithium\core\Libraries;
use lithium\action\Dispatcher;
use lithium\storage\cache\adapter\Apc;
+if (PHP_SAPI === 'cli') {
+ return;
+}
+
/**
- * If APC is not available and the cache directory is not writeable, bail out.
+ * If APC is not available and the cache directory is not writeable, bail out. This block should be
+ * removed post-install, and the cache should be configured with the adapter you plan to use.
*/
-if (!$apcEnabled = Apc::enabled() && !is_writable(LITHIUM_APP_PATH . '/resources/tmp/cache')) {
+$cachePath = Libraries::get(true, 'resources') . '/tmp/cache';
+
+if (!($apcEnabled = Apc::enabled()) && !is_writable($cachePath)) {
return;
}
-Cache::config(array(
- 'default' => array(
- 'adapter' => '\lithium\storage\cache\adapter\\' . ($apcEnabled ? 'Apc' : 'File')
- )
-));
+/**
+ * This configures the default cache, based on whether ot not APC user caching is enabled. If it is
+ * not, file caching will be used. Most of this code is for getting you up and running only, and
+ * should be replaced with a hard-coded configuration, based on the cache(s) you plan to use.
+ */
+$default = array('adapter' => 'File', 'strategies' => array('Serializer'));
+if ($apcEnabled) {
+ $default = array('adapter' => 'Apc');
+}
+Cache::config(compact('default'));
+
+/**
+ * Caches paths for auto-loaded and service-located classes.
+ */
Dispatcher::applyFilter('run', function($self, $params, $chain) {
- if ($cache = Cache::read('default', 'core.libraries')) {
- $cache = (array) unserialize($cache) + Libraries::cache();
+ $key = md5(LITHIUM_APP_PATH) . '.core.libraries';
+
+ if ($cache = Cache::read('default', $key)) {
+ $cache = (array) $cache + Libraries::cache();
Libraries::cache($cache);
}
$result = $chain->next($self, $params, $chain);
if ($cache != Libraries::cache()) {
- Cache::write('default', 'core.libraries', serialize(Libraries::cache()), '+1 day');
+ Cache::write('default', $key, Libraries::cache(), '+1 day');
}
return $result;
});
diff --git a/app/config/bootstrap/connections.php b/app/config/bootstrap/connections.php
new file mode 100644
index 0000000..d8a1826
--- /dev/null
+++ b/app/config/bootstrap/connections.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * ### Configuring backend database connections
+ *
+ * Lithium supports a wide variety relational and non-relational databases, and is designed to allow
+ * and encourage you to take advantage of multiple database technologies, choosing the most optimal
+ * one for each task.
+ *
+ * As with other `Adaptable`-based configurations, each database configuration is defined by a name,
+ * and an array of information detailing what database adapter to use, and how to connect to the
+ * database server. Unlike when configuring other classes, `Connections` uses two keys to determine
+ * which class to select. First is the `'type'` key, which specifies the type of backend to
+ * connect to. For relational databases, the type is set to `'database'`. For HTTP-based backends,
+ * like CouchDB, the type is `'http'`. Some backends have no type grouping, like MongoDB, which is
+ * unique and connects via a custom PECL extension. In this case, the type is set to `'MongoDb'`,
+ * and no `'adapter'` key is specified. In other cases, the `'adapter'` key identifies the unique
+ * adapter of the given type, i.e. `'MySql'` for the `'database'` type, or `'CouchDb'` for the
+ * `'http'` type. Note that while adapters are always specified in CamelCase form, types are
+ * specified either in CamelCase form, or in underscored form, depending on whether an `'adapter'`
+ * key is specified. See the examples below for more details.
+ *
+ * ### Multiple environments
+ *
+ * As with other `Adaptable` classes, `Connections` supports optionally specifying different
+ * configurations per named connection, depending on the current environment. For information on
+ * specifying environment-based configurations, see the `Environment` class.
+ *
+ * @see lithium\core\Adaptable
+ * @see lithium\core\Environment
+ */
+use lithium\data\Connections;
+
+/**
+ * Uncomment this configuration to use MongoDB as your default database.
+ */
+// Connections::add('default', array(
+// 'type' => 'MongoDb',
+// 'host' => 'localhost',
+// 'database' => 'my_app'
+// ));
+
+/**
+ * Uncomment this configuration to use CouchDB as your default database.
+ */
+// Connections::add('default', array(
+// 'type' => 'http',
+// 'adapter' => 'CouchDb',
+// 'host' => 'localhost',
+// 'database' => 'my_app'
+// ));
+
+/**
+ * Uncomment this configuration to use MySQL as your default database.
+ */
+// Connections::add('default', array(
+// 'type' => 'database',
+// 'adapter' => 'MySql',
+// 'host' => 'localhost',
+// 'login' => 'root',
+// 'password' => '',
+// 'database' => 'my_app'
+// ));
+
+?>
\ No newline at end of file
diff --git a/app/config/bootstrap/errors.php b/app/config/bootstrap/errors.php
new file mode 100644
index 0000000..8354478
--- /dev/null
+++ b/app/config/bootstrap/errors.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use lithium\core\ErrorHandler;
+use lithium\action\Response;
+use lithium\net\http\Media;
+
+ErrorHandler::apply('lithium\action\Dispatcher::run', array(), function($info, $params) {
+ $response = new Response(array(
+ 'request' => $params['request'],
+ 'status' => $info['exception']->getCode()
+ ));
+
+ Media::render($response, compact('info', 'params'), array(
+ 'controller' => '_errors',
+ 'template' => 'development',
+ 'layout' => 'error',
+ 'request' => $params['request']
+ ));
+ return $response;
+});
+
+?>
\ No newline at end of file
diff --git a/app/config/bootstrap/g11n.php b/app/config/bootstrap/g11n.php
index 2f10e11..e96241f 100644
--- a/app/config/bootstrap/g11n.php
+++ b/app/config/bootstrap/g11n.php
@@ -6,17 +6,21 @@
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
-namespace lithium;
-
-use \lithium\core\Environment;
-use \lithium\g11n\Locale;
-use \lithium\g11n\Catalog;
-use \lithium\g11n\Message;
-use \lithium\util\Inflector;
-use \lithium\util\Validator;
-use \lithium\net\http\Media;
-use \lithium\action\Dispatcher as ActionDispatcher;
-use \lithium\console\Dispatcher as ConsoleDispatcher;
+/**
+ * This bootstrap file contains class configuration for all aspects of globalizing your application,
+ * including localization of text, validation rules, setting timezones and character inflections,
+ * and identifying a user's locale.
+ */
+use lithium\core\Libraries;
+use lithium\core\Environment;
+use lithium\g11n\Locale;
+use lithium\g11n\Catalog;
+use lithium\g11n\Message;
+use lithium\util\Inflector;
+use lithium\util\Validator;
+use lithium\net\http\Media;
+use lithium\action\Dispatcher as ActionDispatcher;
+use lithium\console\Dispatcher as ConsoleDispatcher;
/**
* Sets the default timezone used by all date/time functions.
@@ -31,65 +35,55 @@ date_default_timezone_set('UTC');
* globalization related settings.
*
* The environment settings are:
- * - `'locale'` The effective locale. Defaults to `'en'`.
- * - `'availableLocales'` Application locales available. Defaults to `array('en')`.
+ *
+ * - `'locale'` The effective locale.
+ * - `'locales'` Application locales available mapped to names. The available locales are used
+ * to negotiate he effective locale, the names can be used i.e. when displaying
+ * a menu for choosing the locale to users.
*/
-Environment::set('production', array(
- 'locale' => 'en',
- 'availableLocales' => array('en')
-));
-Environment::set('development', array(
- 'locale' => 'en',
- 'availableLocales' => array('en')
-));
-Environment::set('test', array(
- 'locale' => 'en',
- 'availableLocales' => array('en')
-));
+$locale = 'en';
+$locales = array('en' => 'English');
+
+Environment::set('production', compact('locale', 'locales'));
+Environment::set('development', compact('locale', 'locales'));
+Environment::set('test', array('locale' => 'en', 'locales' => array('en' => 'English')));
/**
* Globalization (g11n) catalog configuration. The catalog allows for obtaining and
* writing globalized data. Each configuration can be adjusted through the following settings:
*
- * - `'adapter' The name of a supported adapter. The builtin adapters are _memory_ (a
- * simple adapter good for runtime data and testing), _gettext_, _cldr_ (for
- * interfacing with Unicode's common locale data repository) and _code_ (used mainly for
+ * - `'adapter'` _string_: The name of a supported adapter. The builtin adapters are `Memory` (a
+ * simple adapter good for runtime data and testing), `Php`, `Gettext`, `Cldr` (for
+ * interfacing with Unicode's common locale data repository) and `Code` (used mainly for
* extracting message templates from source code).
*
- * - `'path'` All adapters with the exception of the _memory_ adapter require a directory
+ * - `'path'` All adapters with the exception of the `Memory` adapter require a directory
* which holds the data.
*
* - `'scope'` If you plan on using scoping i.e. for accessing plugin data separately you
- * need to specify a scope for each configuration, except for those using the _memory_,
- * _php_ or _gettext_ adapter which handle this internally.
+ * need to specify a scope for each configuration, except for those using the `Memory`,
+ * `Php` or `Gettext` adapter which handle this internally.
*/
Catalog::config(array(
'runtime' => array(
'adapter' => 'Memory'
),
-// 'app' => array(
-// 'adapter' => 'Gettext',
-// 'path' => LITHIUM_APP_PATH . '/resources/g11n'
-// ),
+ // 'app' => array(
+ // 'adapter' => 'Gettext',
+ // 'path' => Libraries::get(true, 'resources') . '/g11n'
+ // ),
'lithium' => array(
'adapter' => 'Php',
'path' => LITHIUM_LIBRARY_PATH . '/lithium/g11n/resources/php'
)
-));
-
-/**
- * Globalization runtime data. You can add globalized data during runtime utilizing a
- * configuration set up to use the _memory_ adapter.
- */
-$data = function($n) { return $n != 1 ? 1 : 0; };
-Catalog::write('message.plural', 'root', $data, array('name' => 'runtime'));
+) + Catalog::config());
/**
* Integration with `Inflector`.
*/
-// Inflector::rules('transliteration', Catalog::read('inflection.transliteration', 'en'));
+// Inflector::rules('transliteration', Catalog::read(true, 'inflection.transliteration', 'en'));
-/*
+/**
* Inflector configuration examples. If your application has custom singular or plural rules, or
* extra non-ASCII characters to transliterate, you can configure that by uncommenting the lines
* below.
@@ -121,9 +115,9 @@ Media::applyFilter('_handle', function($self, $params, $chain) {
* Integration with `Validator`. You can load locale dependent rules into the `Validator`
* by specifying them manually or retrieving them with the `Catalog` class.
*/
-Validator::add('phone', Catalog::read('validation.phone', 'en_US'));
-Validator::add('postalCode', Catalog::read('validation.postalCode', 'en_US'));
-Validator::add('ssn', Catalog::read('validation.ssn', 'en_US'));
+foreach (array('phone', 'postalCode', 'ssn') as $name) {
+ Validator::add($name, Catalog::read(true, "validation.{$name}", 'en_US'));
+}
/**
* Intercepts dispatching processes in order to set the effective locale by using
@@ -140,6 +134,7 @@ ActionDispatcher::applyFilter('_callable', function($self, $params, $chain) {
Environment::set(Environment::get(), array('locale' => $request->locale));
return $controller;
});
+
ConsoleDispatcher::applyFilter('_callable', function($self, $params, $chain) {
$request = $params['request'];
$command = $chain->next($self, $params, $chain);
diff --git a/app/config/bootstrap/libraries.php b/app/config/bootstrap/libraries.php
index 4244a53..e11a703 100644
--- a/app/config/bootstrap/libraries.php
+++ b/app/config/bootstrap/libraries.php
@@ -6,6 +6,72 @@
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
+/**
+ * The libraries file is where you configure the various plugins, frameworks, and other libraries
+ * to be used by your application, including your application itself. This file also defines some
+ * global constants used to tell Lithium where to find your application and support libraries
+ * (including Lithium itself). It uses the `Libraries` class to add configurations for the groups of
+ * classes used in your app.
+ *
+ * In Lithium, a _library_ is any collection of classes located in a single base directory, which
+ * all share the same class-to-file naming convention, and usually a common class or namespace
+ * prefix. While all collections of classes are considered libraries, there are two special types of
+ * libraries:
+ *
+ * - **Applications**: Applications are libraries which follow the organizational conventions that
+ * Lithium defines for applications (see `Libraries::locate()` and `Libraries::paths()`), and
+ * which also include a web-accessible document root (i.e. the `webroot/` folder), and can
+ * dispatch HTTP requests (i.e. through `webroot/index.php`).
+ *
+ * - **Plugins**: Plugins are libraries which generally follow the same organizational conventions
+ * as applications, but are designed to be used within the context of another application. They
+ * _may_ include a public document root for supporting assets, but this requires a symlink from
+ * `libraries/<plugin-name>/webroot` to `<app-name>/webroot/<plugin-name>` (recommended for
+ * production), or a media filter to load plugin resources (see `/config/bootstrap/media.php`).
+ *
+ * Note that a library can be designed as both an application and a plugin, but this requires some
+ * special considerations in the bootstrap process, such as removing any `require` statements, and
+ * conditionally defining the constants below.
+ *
+ * By default, libraries are stored in the base `/libraries` directory, or in the
+ * application-specific `<app-name>/libraries` directory. Libraries can be loaded from either place
+ * without additional configuration, but note that if the same library is in both directories, the
+ * application-specific `libraries` directory will override the global one.
+ *
+ * The one exception to this is the _primary_ library, which is an application configured with
+ * `'default' => true` (see below); this library uses the `LITHIUM_APP_PATH` constant (also defined
+ * below) as its path. Note, however, that any library can be overridden with an arbitrary path by
+ * passing the `'path'` key to its configuration. See `Libraries::add()` for more options.
+ *
+ * @see lithium\core\Libraries
+ */
+
+/**
+ * This is the path to your application's directory. It contains all the sub-folders for your
+ * application's classes and files. You don't need to change this unless your webroot folder is
+ * stored outside of your app folder.
+ */
+define('LITHIUM_APP_PATH', dirname(dirname(__DIR__)));
+
+/**
+ * This is the path to the class libraries used by your application, and must contain a copy of the
+ * Lithium core. By default, this directory is named `libraries`, and resides in the same
+ * directory as your application. If you use the same libraries in multiple applications, you can
+ * set this to a shared path on your server.
+ */
+define('LITHIUM_LIBRARY_PATH', dirname(LITHIUM_APP_PATH) . '/libraries');
+
+/**
+ * Locate and load Lithium core library files. Throws a fatal error if the core can't be found.
+ * If your Lithium core directory is named something other than `lithium`, change the string below.
+ */
+if (!include LITHIUM_LIBRARY_PATH . '/lithium/core/Libraries.php') {
+ $message = "Lithium core could not be found. Check the value of LITHIUM_LIBRARY_PATH in ";
+ $message .= __FILE__ . ". It should point to the directory containing your ";
+ $message .= "/libraries directory.";
+ throw new ErrorException($message);
+}
+
use lithium\core\Libraries;
/**
@@ -21,7 +87,8 @@ require LITHIUM_LIBRARY_PATH . '/lithium/util/Inflector.php';
require LITHIUM_LIBRARY_PATH . '/lithium/util/String.php';
require LITHIUM_LIBRARY_PATH . '/lithium/core/Adaptable.php';
require LITHIUM_LIBRARY_PATH . '/lithium/core/Environment.php';
-require LITHIUM_LIBRARY_PATH . '/lithium/net/http/Base.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/net/Message.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/net/http/Message.php';
require LITHIUM_LIBRARY_PATH . '/lithium/net/http/Media.php';
require LITHIUM_LIBRARY_PATH . '/lithium/net/http/Request.php';
require LITHIUM_LIBRARY_PATH . '/lithium/net/http/Response.php';
@@ -51,7 +118,7 @@ Libraries::add('lithium');
Libraries::add('app', array('default' => true));
/**
- * Add some plugins
+ * Add some plugins:
*/
// Libraries::add('li3_docs');
diff --git a/app/config/bootstrap/media.php b/app/config/bootstrap/media.php
index e7bd37e..c4a42c6 100644
--- a/app/config/bootstrap/media.php
+++ b/app/config/bootstrap/media.php
@@ -21,9 +21,9 @@
* return $posts->to('json');
* }}}
*/
-use \lithium\util\Collection;
+use lithium\util\Collection;
-Collection::formats('\lithium\net\http\Media');
+Collection::formats('lithium\net\http\Media');
/**
* This filter is a convenience method which allows you to automatically route requests for static
@@ -34,26 +34,26 @@ Collection::formats('\lithium\net\http\Media');
* plugin's `webroot` directory into your main application's `webroot` directory, or adding routing
* rules in your web server's configuration.
*/
-use \lithium\action\Dispatcher;
-use \lithium\core\Libraries;
-use \lithium\net\http\Media;
+use lithium\action\Dispatcher;
+use lithium\action\Response;
+use lithium\net\http\Media;
Dispatcher::applyFilter('_callable', function($self, $params, $chain) {
- list($plugin, $asset) = explode('/', $params['request']->url, 2) + array("", "");
- if ($asset && $library = Libraries::get($plugin)) {
- $asset = "{$library['path']}/webroot/{$asset}";
+ list($library, $asset) = explode('/', $params['request']->url, 2) + array("", "");
- if (file_exists($asset)) {
- return function () use ($asset) {
- $info = pathinfo($asset);
- $type = Media::type($info['extension']);
- header("Content-type: {$type['content']}");
- return file_get_contents($asset);
- };
- }
+ if ($asset && ($path = Media::webroot($library)) && file_exists($file = "{$path}/{$asset}")) {
+ return function() use ($file) {
+ $info = pathinfo($file);
+ $media = Media::type($info['extension']);
+ $content = (array) $media['content'];
+
+ return new Response(array(
+ 'headers' => array('Content-type' => reset($content)),
+ 'body' => file_get_contents($file)
+ ));
+ };
}
return $chain->next($self, $params, $chain);
});
-
?>
\ No newline at end of file
diff --git a/app/config/bootstrap/session.php b/app/config/bootstrap/session.php
new file mode 100644
index 0000000..be957e0
--- /dev/null
+++ b/app/config/bootstrap/session.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * This configures your session storage. The Cookie storage adapter must be connected first, since
+ * it intercepts any writes where the `'expires'` key is set in the options array.
+ */
+use lithium\storage\Session;
+
+Session::config(array(
+ // 'cookie' => array('adapter' => 'Cookie'),
+ 'default' => array('adapter' => 'Php')
+));
+
+/**
+ * Uncomment the lines below to enable forms-based authentication. This configuration will attempt
+ * to authenticate users against a `Users` model. In a controller, run
+ * `Auth::check('default', $this->request)` to authenticate a user. This will check the POST data of
+ * the request (`lithium\action\Request::$data`) to see if the fields match the `'fields'` key of
+ * the configuration below. If successful, it will write the data returned from `Users::first()` to
+ * the session using the default session configuration.
+ *
+ * Once the session data is written, you can call `Auth::check('default')` to check authentication
+ * status or retrieve the user's data from the session. Call `Auth::clear('default')` to remove the
+ * user's authentication details from the session. This effectively logs a user out of the system.
+ * To modify the form input that the adapter accepts, or how the configured model is queried, or how
+ * the data is stored in the session, see the `Form` adapter API or the `Auth` API, respectively.
+ *
+ * @see lithium\security\auth\adapter\Form
+ * @see lithium\action\Request::$data
+ * @see lithium\security\Auth
+ */
+// use lithium\security\Auth;
+
+// Auth::config(array(
+// 'default' => array(
+// 'adapter' => 'Form',
+// 'model' => 'Users',
+// 'fields' => array('username', 'password')
+// )
+// ));
+
+?>
\ No newline at end of file
diff --git a/app/config/connections.php b/app/config/connections.php
deleted file mode 100644
index fba3a15..0000000
--- a/app/config/connections.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-use \lithium\data\Connections;
-
-// Connections::add('default', array(
-// 'type' => 'database',
-// 'adapter' => 'MySql',
-// 'host' => 'localhost',
-// 'login' => 'root',
-// 'password' => '',
-// 'database' => 'app_name'
-// ));
-
-?>
\ No newline at end of file
diff --git a/app/config/routes.php b/app/config/routes.php
index 6caa739..6e695cc 100644
--- a/app/config/routes.php
+++ b/app/config/routes.php
@@ -6,37 +6,79 @@
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
-use \lithium\net\http\Router;
+/**
+ * The routes file is where you define your URL structure, which is an important part of the
+ * [information architecture](http://en.wikipedia.org/wiki/Information_architecture) of your
+ * application. Here, you can use _routes_ to match up URL pattern strings to a set of parameters,
+ * usually including a controller and action to dispatch matching requests to. For more information,
+ * see the `Router` and `Route` classes.
+ *
+ * @see lithium\net\http\Router
+ * @see lithium\net\http\Route
+ */
+use lithium\net\http\Router;
+use lithium\core\Environment;
/**
- * Uncomment the line below to enable routing for admin actions.
- * @todo Implement me.
+ * Here, we are connecting `'/'` (the base path) to controller called `'Pages'`,
+ * its action called `view()`, and we pass a param to select the view file
+ * to use (in this case, `/views/pages/home.html.php`; see `app\controllers\PagesController`
+ * for details).
+ *
+ * @see app\controllers\PagesController
*/
-// Router::namespace('/admin', array('admin' => true));
+Router::connect('/', 'Pages::view');
/**
- * Here, we are connecting '/' (base path) to controller called 'Pages',
- * its action called 'view', and we pass a param to select the view file
- * to use (in this case, /app/views/pages/home.html.php)...
+ * Connect the rest of `PagesController`'s URLs. This will route URLs like `/pages/about` to
+ * `PagesController`, rendering `/views/pages/about.html.php` as a static page.
*/
-Router::connect('/', array('controller' => 'pages', 'action' => 'view', 'home'));
+Router::connect('/pages/{:args}', 'Pages::view');
/**
- * ...and connect the rest of 'Pages' controller's urls.
+ * Add the testing routes. These routes are only connected in non-production environments, and allow
+ * browser-based access to the test suite for running unit and integration tests for the Lithium
+ * core, as well as your own application and any other loaded plugins or frameworks. Browse to
+ * [http://path/to/app/test](/test) to run tests.
*/
-Router::connect('/pages/{:args}', array('controller' => 'pages', 'action' => 'view'));
+if (!Environment::is('production')) {
+ Router::connect('/test/{:args}', array('controller' => 'lithium\test\Controller'));
+ Router::connect('/test', array('controller' => 'lithium\test\Controller'));
+}
/**
- * Connect the testing routes.
+ * ### Database object routes
+ *
+ * The routes below are used primarily for accessing database objects, where `{:id}` corresponds to
+ * the primary key of the database object, and can be accessed in the controller as
+ * `$this->request->id`.
+ *
+ * If you're using a relational database, such as MySQL, SQLite or Postgres, where the primary key
+ * is an integer, uncomment the routes below to enable URLs like `/posts/edit/1138`,
+ * `/posts/view/1138.json`, etc.
*/
-Router::connect('/test/{:args}', array('controller' => '\lithium\test\Controller'));
-Router::connect('/test', array('controller' => '\lithium\test\Controller'));
+// Router::connect('/{:controller}/{:action}/{:id:\d+}.{:type}', array('id' => null));
+// Router::connect('/{:controller}/{:action}/{:id:\d+}');
/**
- * Finally, connect the default routes.
+ * If you're using a document-oriented database, such as CouchDB or MongoDB, or another type of
+ * database which uses 24-character hexidecimal values as primary keys, uncomment the routes below.
+ */
+// Router::connect('/{:controller}/{:action}/{:id:[0-9a-f]{24}}.{:type}', array('id' => null));
+// Router::connect('/{:controller}/{:action}/{:id:[0-9a-f]{24}}');
+
+/**
+ * Finally, connect the default route. This route acts as a catch-all, intercepting requests in the
+ * following forms:
+ *
+ * - `/foo/bar`: Routes to `FooController::bar()` with no parameters passed.
+ * - `/foo/bar/param1/param2`: Routes to `FooController::bar('param1, 'param2')`.
+ * - `/foo`: Routes to `FooController::index()`, since `'index'` is assumed to be the action if none
+ * is otherwise specified.
+ *
+ * In almost all cases, custom routes should be added above this one, since route-matching works in
+ * a top-down fashion.
*/
-Router::connect('/{:controller}/{:action}/{:id:[0-9]+}.{:type}', array('id' => null));
-Router::connect('/{:controller}/{:action}/{:id:[0-9]+}');
Router::connect('/{:controller}/{:action}/{:args}');
?>
\ No newline at end of file
diff --git a/app/controllers/HelloWorldController.php b/app/controllers/HelloWorldController.php
index 5f43d1c..8dad480 100644
--- a/app/controllers/HelloWorldController.php
+++ b/app/controllers/HelloWorldController.php
@@ -5,7 +5,7 @@ namespace app\controllers;
class HelloWorldController extends \lithium\action\Controller {
public function index() {
- $this->render(array('layout' => false));
+ return $this->render(array('layout' => false));
}
public function to_string() {
@@ -13,7 +13,7 @@ class HelloWorldController extends \lithium\action\Controller {
}
public function to_json() {
- $this->render(array('json' => 'Hello World'));
+ return $this->render(array('json' => 'Hello World'));
}
}
diff --git a/app/controllers/PagesController.php b/app/controllers/PagesController.php
index 3a61c51..03c3b21 100644
--- a/app/controllers/PagesController.php
+++ b/app/controllers/PagesController.php
@@ -1,16 +1,33 @@
<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
namespace app\controllers;
+/**
+ * This controller is used for serving static pages by name, which are located in the `/views/pages`
+ * folder.
+ *
+ * A Lithium application's default routing provides for automatically routing and rendering
+ * static pages using this controller. The default route (`/`) will render the `home` template, as
+ * specified in the `view()` action.
+ *
+ * Additionally, any other static templates in `/views/pages` can be called by name in the URL. For
+ * example, browsing to `/pages/about` will render `/views/pages/about.html.php`, if it exists.
+ *
+ * Templates can be nested within directories as well, which will automatically be accounted for.
+ * For example, browsing to `/pages/about/company` will render
+ * `/views/pages/about/company.html.php`.
+ */
class PagesController extends \lithium\action\Controller {
public function view() {
- $path = func_get_args();
-
- if (empty($path)) {
- $path = array('home');
- }
- $this->render(join('/', $path));
+ $path = func_get_args() ?: array('home');
+ return $this->render(array('template' => join('/', $path)));
}
}
diff --git a/app/libraries/_source/empty b/app/libraries/_source/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/libraries/empty b/app/libraries/empty
deleted file mode 100644
index e69de29..0000000
diff --git a/app/views/_errors/development.html.php b/app/views/_errors/development.html.php
new file mode 100644
index 0000000..18654f6
--- /dev/null
+++ b/app/views/_errors/development.html.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use lithium\analysis\Debugger;
+use lithium\analysis\Inspector;
+
+$exception = $info['exception'];
+$replace = array('<?php', '?>', '<code>', '</code>', "\n");
+$context = 5;
+
+/**
+ * Set Lithium-esque colors for syntax highlighing.
+ */
+ini_set('highlight.string', '#4DDB4A');
+ini_set('highlight.comment', '#D42AAE');
+ini_set('highlight.keyword', '#D42AAE');
+ini_set('highlight.default', '#3C96FF');
+ini_set('highlight.htm', '#FFFFFF');
+
+$stack = Debugger::trace(array('format' => 'array', 'trace' => $exception->getTrace()));
+
+array_unshift($stack, array(
+ 'functionRef' => '[exception]',
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine()
+));
+
+?>
+<h3>Exception</h3>
+
+<div class="lithium-exception-class">
+ <?=get_class($exception);?>
+
+ <?php if ($code = $exception->getCode()): ?>
+ <span class="code">(code <?=$code; ?>)</span>
+ <?php endif ?>
+</div>
+
+<div class="lithium-exception-message"><?=$exception->getMessage(); ?></div>
+
+<h3 id="source">Source</h3>
+
+<div id="sourceCode"></div>
+
+<h3>Stack Trace</h3>
+
+<div class="lithium-stack-trace">
+ <ol>
+ <?php foreach ($stack as $id => $frame): ?>
+ <?php
+ $location = "{$frame['file']}: {$frame['line']}";
+ $lines = range($frame['line'] - $context, $frame['line'] + $context);
+ $code = Inspector::lines($frame['file'], $lines);
+ ?>
+ <li>
+ <tt><a href="#source" id="<?=$id; ?>" class="display-source-excerpt">
+ <?=$frame['functionRef']; ?>
+ </a></tt>
+ <div id="sourceCode<?=$id; ?>" style="display: none;">
+
+ <div class="lithium-exception-location">
+ <?=$location; ?>
+ </div>
+
+ <div class="lithium-code-dump">
+ <pre><code><?php
+ foreach ($code as $num => $content):
+ $numPad = str_pad($num, 3, ' ');
+ $content = str_ireplace(array('<?php', '?>'), '', $content);
+ $content = highlight_string("<?php {$numPad}{$content} ?>", true);
+ $content = str_replace($replace, '', $content);
+
+ if ($frame['line'] === $num):
+ ?><span class="code-highlight"><?php
+ endif;?><?php echo "{$content}\n"; ?><?php
+ if ($frame['line'] === $num):
+ ?></span><?php
+ endif;
+
+ endforeach;
+ ?></code></pre>
+ </div>
+ </div>
+ </li>
+ <?php endforeach; ?>
+ </ol>
+</div>
+
+<script type="text/javascript">
+ window.onload = function() {
+ var $ = function() { return document.getElementById.apply(document, arguments); };
+ var links = document.getElementsByTagName('a');
+
+ for (i = 0; i < links.length; i++) {
+ if (links[i].className.indexOf('display-source-excerpt') >= 0) {
+ links[i].onclick = function() {
+ $('sourceCode').innerHTML = $('sourceCode' + this.id).innerHTML;
+ }
+ }
+ }
+ $('sourceCode').innerHTML = $('sourceCode0').innerHTML;
+ }
+</script>
\ No newline at end of file
diff --git a/app/views/layouts/default.html.php b/app/views/layouts/default.html.php
index 847dfdb..6f76bd2 100644
--- a/app/views/layouts/default.html.php
+++ b/app/views/layouts/default.html.php
@@ -11,7 +11,7 @@
<head>
<?php echo $this->html->charset();?>
<title>Application > <?php echo $this->title(); ?></title>
- <?php echo $this->html->style(array('lithium', 'debug')); ?>
+ <?php echo $this->html->style(array('debug', 'lithium')); ?>
<?php echo $this->scripts(); ?>
<?php echo $this->html->link('Icon', null, array('type' => 'icon')); ?>
</head>
@@ -20,7 +20,7 @@
<div id="header">
<h1>Application</h1>
<h2>
- Powered by <?php echo $this->html->link('Lithium', 'http://li3.rad-dev.org'); ?>.
+ Powered by <?php echo $this->html->link('Lithium', 'http://lithify.me/'); ?>.
</h2>
</div>
<div id="content">
diff --git a/app/views/layouts/default.xml.php b/app/views/layouts/default.xml.php
index 3eebc6a..4a92323 100644
--- a/app/views/layouts/default.xml.php
+++ b/app/views/layouts/default.xml.php
@@ -6,5 +6,5 @@
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
?>
-<?=$this->xml->header();?>
-<?=$this->content;?>
\ No newline at end of file
+<?php echo '<' . '?xml version="1.0" ?' . '>'; ?>
+<?=$this->content;?>
diff --git a/app/views/layouts/error.html.php b/app/views/layouts/error.html.php
new file mode 100644
index 0000000..2f26d97
--- /dev/null
+++ b/app/views/layouts/error.html.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * This layout is used to render error pages in both development and production. It is recommended
+ * that you maintain a separate, simplified layout for rendering errors that does not involve any
+ * complex logic or dynamic data, which could potentially trigger recursive errors.
+ */
+?>
+<!doctype html>
+<html>
+<head>
+ <?php echo $this->html->charset(); ?>
+ <title>Unhandled exception</title>
+ <?php echo $this->html->style(array('debug', 'lithium')); ?>
+ <?php echo $this->scripts(); ?>
+ <?php echo $this->html->link('Icon', null, array('type' => 'icon')); ?>
+</head>
+<body class="app">
+ <div id="container">
+ <div id="header">
+ <h1>An unhandled exception was thrown</h1>
+ <h3>Configuration</h3>
+ <p>
+ This layout can be changed by modifying
+ <code><?php
+ echo realpath(LITHIUM_APP_PATH . '/views/layouts/error.html.php');
+ ?></code>
+ </p><p>
+ To modify your error-handling configuration, see
+ <code><?php
+ echo realpath(LITHIUM_APP_PATH . '/config/bootstrap/errors.php');
+ ?></code>
+ </p>
+ </div>
+ <div id="content">
+ <?php echo $this->content(); ?>
+ </div>
+ </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/app/views/pages/home.html.php b/app/views/pages/home.html.php
index ae4ce38..b1fa59a 100644
--- a/app/views/pages/home.html.php
+++ b/app/views/pages/home.html.php
@@ -6,53 +6,105 @@
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
-use \lithium\data\Connections;
+use lithium\core\Libraries;
+use lithium\data\Connections;
$checkName = null;
-$checkStatus = array();
+$checkStatus = $solutions = array();
+
+$notify = function($status, $message, $solution = null) use (&$checkName, &$checkStatus, &$solutions) {
+ $checkStatus[$checkName] = $status === true;
+
+ if (!is_string($status)) {
+ $status = $status ? 'success' : 'fail';
+ }
-$notify = function($status, $message) use (&$checkName, &$checkStatus) {
- $checkStatus[$checkName] = $status;
- $status = $status ? 'success' : 'fail';
$message = is_array($message) ? join("\n<br />", $message) : $message;
+
+ if (!empty($solution)) {
+ $default = array(
+ 'id' => 'help-' . $checkName,
+ 'title' => $checkName,
+ 'content' => null
+ );
+ if (is_array($solution['content'])) {
+ $solution['content'] = join("\n<br />", $solution['content']);
+ }
+ $solutions[$checkName] = $solution += $default;
+
+ }
return "<div class=\"test-result test-result-{$status}\">{$message}</div>";
};
$sanityChecks = array(
'resourcesWritable' => function() use ($notify) {
- if (is_writable($path = LITHIUM_APP_PATH . '/resources')) {
- return $notify(true, 'Your application\'s resources directory is writable.');
+ if (is_writable($path = realpath(Libraries::get(true, 'resources')))) {
+ return $notify(true, 'Resources directory is writable.');
}
return $notify(false, array(
- "Your resource path (<code>$path</code>) is not writeable. " .
+ "Your resource path (<code>{$path}</code>) is not writeable. " .
"To fix this on *nix and Mac OSX, run the following from the command line:",
"<code>chmod -R 0777 {$path}</code>"
));
},
'database' => function() use ($notify) {
- if (!$config = Connections::get('default')) {
- return $notify(false, array(
- 'No default database connection defined. To create a database connection, ' .
- 'edit the file <code>' . LITHIUM_APP_PATH . '/config/bootstrap.php</code>, and ' .
- 'uncomment the following line:',
- '<code>require __DIR__ . \'/connections.php\';</code>',
- 'Then, edit the file <code>' . LITHIUM_APP_PATH . '/config/connections.php</code>.'
+ $config = Connections::config();
+ $boot = realpath(LITHIUM_APP_PATH . '/config/bootstrap.php');
+ $connections = realpath(LITHIUM_APP_PATH . '/config/bootstrap/connections.php');
+
+ if (empty($config)) {
+ return $notify('notice', array('No database connections defined.'), array(
+ 'title' => 'Database Connections',
+ 'content' => array(
+ 'To create a database connection, edit the file <code>' . $boot . '</code>, ',
+ 'and uncomment the following line:',
+ '<pre><code>require __DIR__ . \'/bootstrap/connections.php\';</code></pre>',
+ 'Then, edit the file <code>' . $connections . '</code>.'
+ )
));
}
- return $notify(true, 'Default database connection configured.');
- },
- 'databaseConnected' => function() use ($notify, &$checkStatus) {
- if (!$checkStatus['database']) {
- return;
- }
- if (@Connections::get('default')->connect()) {
- return $notify(true, 'Connection to default database verified.');
- }
- return $notify(false, array(
- 'Could not connect to default database. Please check the ' .
- 'settings in <code>' . LITHIUM_APP_PATH . '/config/connections.php</code>.'
- ));
+ return $notify(true, 'Database connection(s) configured.');
},
+ // 'databaseEnabled' => function() use ($notify, &$checkStatus) {
+ // if (!$checkStatus['database']) {
+ // return;
+ // }
+ // $results = array();
+ // $config = Connections::config();
+ // foreach ($config as $name => $options) {
+ // $enabled = Connections::enabled($name);
+ // if (!$enabled) {
+ // $results[] = $notify('exception', "Database for <code>{$options}</code> is not enabled.");
+ // }
+ // }
+ // if (empty($results)) {
+ // $results[] = $notify(true, "Database(s) enabled.");
+ // }
+ // return implode("\n", $results);
+ // },
+ // 'databaseConnected' => function() use ($notify, &$checkStatus) {
+ // if (!$checkStatus['database']) {
+ // return;
+ // }
+ // $results = array();
+ // $config = Connections::config();
+ // foreach ($config as $name => $options) {
+ // $enabled = Connections::enabled($name);
+ // if ($enabled) {
+ // $connection = Connections::get($name)->connect();
+ // if ($connection) {
+ // $results[] = $notify(
+ // true, "Connection to <code>{$name}</code> database verified."
+ // );
+ // } else {
+ // $results[] = $notify(
+ // false, "Could not connect to <code>{$name}</code> database."
+ // );
+ // }
+ // }
+ // }
+ // return implode("\n", $results);
+ // },
'magicQuotes' => function() use ($notify) {
if (get_magic_quotes_gpc() === 0) {
return;
@@ -70,43 +122,51 @@ $sanityChecks = array(
'Register globals is enabled in your PHP configuration. Please set <code>' .
'register_globals = Off</code> in your <code>php.ini</code> settings.'
));
- }
+ },
);
?>
-<h3><?php echo $this->title('home'); ?></h3>
+<?php
+
+foreach ($sanityChecks as $checkName => $check) {
+ echo $check();
+}
+
+?>
+<h3>Getting Started</h3>
<p>
This is your application's default home page. To change this template, edit the file
- <code><?php echo LITHIUM_APP_PATH . '/views/pages/home.html.php'; ?></code>.
+ <code><?php echo realpath(LITHIUM_APP_PATH . '/views/pages/home.html.php'); ?></code>.
</p>
+<h4>Layout</h4>
<p>
- To change the application's <em>layout</em> (the file containing the
- header, footer and default styles), edit the file
- <code><?php echo LITHIUM_APP_PATH . '/views/layouts/default.html.php'; ?></code>.
+ To change the application's
+ <em><a href="http://lithify.me/en/docs/lithium/template">layout</a></em> (the file containing
+ the header, footer and default styles), edit the file
+ <code><?php echo realpath(LITHIUM_APP_PATH . '/views/layouts/default.html.php'); ?></code>.
</p>
+<h4>Routing</h4>
<p>
To change the <em><a href="http://lithify.me/docs/lithium/net/http/Router">routing</a></em> of
the application's default page, edit the file
- <code><?php echo LITHIUM_APP_PATH . '/config/routes.php'; ?></code>.
+ <code><?php echo realpath(LITHIUM_APP_PATH . '/config/routes.php'); ?></code>.
</p>
-<h3>system check</h3>
-
-<?php
-
-foreach ($sanityChecks as $checkName => $check) {
- echo $check();
-}
-
-?>
+<?php if ($solutions) { ?>
+ <?php foreach ($solutions as $solution) { ?>
+ <h4 id="<?php echo $solution['id']; ?>"><?php echo $solution['title']; ?></h4>
+ <p><?php echo $solution['content']; ?></p>
+ <?php } ?>
+<?php } ?>
-<h4>additional resources</h4>
+<h4>Additional Resources</h4>
<ul>
<li><a href="http://lithify.me/docs/lithium">Lithium API</a></li>
- <li><a href="http://rad-dev.org/lithium/wiki">Lithium Development Wiki</a></li>
- <li><a href="http://rad-dev.org/lithium">Lithium Source</a></li>
+ <li><a href="http://sphere.lithify.me/">Lithium Community</a></li>
+ <li><a href="http://dev.lithify.me/lithium/wiki">Lithium Development Wiki</a></li>
+ <li><a href="http://dev.lithify.me/lithium/source">Lithium Source</a></li>
<li><a href="irc://irc.freenode.net/#li3">#li3 irc channel</a></li>
</ul>
\ No newline at end of file
diff --git a/app/webroot/css/debug.css b/app/webroot/css/debug.css
index 31d4f57..ed5c4b8 100644
--- a/app/webroot/css/debug.css
+++ b/app/webroot/css/debug.css
@@ -11,120 +11,370 @@
body.test-dashboard {
font-family: Helvetica, Arial, sans-serif;
- font-size: 90%;
+ font-size: 16px;
margin: 0;
+ min-width: 800px;
}
body.test-dashboard a {
color: #333;
}
-body.test-dashboard ul {
- margin: 0.2em 0.5em;
+body.test-dashboard #header h1 {
+ margin: 0;
+ float:right;
+ font-weight: normal;
+}
+
+body.test-dashboard #header h1 a {
+ text-decoration: none;
+ display: block;
+ padding: .45em 0.75em 0 0;
+ color: rgba(0,0,0,.15);
+}
+
+body.test-dashboard .triangle:before {
+ content: '\25B2';
+ font-size: 1em;
+}
+
+body.test-dashboard #header {
+ padding: 0;
}
-body.test-dashboard li {
- margin: 0.4em 1em;
+body.test-dashboard #header:after {
+ display: block;
+ content: ' ';
+ clear: both;
}
-body.test-dashboard h1 {
+body.test-dashboard .article {
+ clear:both;
+}
+
+body.test-dashboard .test-content {
+ float:left;
+ padding: 2em 2% 4em;
+ width: 74%;
+}
+
+.test-content h2 {
+ font-weight: normal;
+ font-size: 1.45em;
+ margin-bottom: .5em;
+ float: left;
+}
+.test-content h2 span {
+ color: #bbb;
+ display: block;
+ font-size: .55em;
+}
+.test-content h3 {
+ font-weight: normal;
+ margin: 1.5em 0 1em;
+}
+
+body.test-dashboard a.test-button,
+body.test-dashboard a.test-button:link,
+body.test-dashboard a.test-button:visited,
+body.test-dashboard a.test-button:hover,
+body.test-dashboard a.test-button:active {
+ display: block;
+ float: right;
+ font-weight: bold;
+ font-size: 1.25em;
+ background-color:#f5f5f5;
+ border-color: #e6e6e6;
+ color: #999;
+ padding: .5em 1em;
margin: 0;
- padding: 8px 0 4px 14px;
- background: #B5B4A4;
+ background-color: white;
+ border: 1px solid #e5e5e5;
+ text-decoration: none;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ -moz-box-shadow: 0 0 6px rgba(0,0,0,.1);
+ -webkit-box-shadow: 0 0 6px rgba(0,0,0,.1);
+ box-shadow: 0 0 6px rgba(0,0,0,.1);
+}
+
+body.test-dashboard a.test-button:hover,
+body.test-dashboard a.test-button:active {
+ color: black;
+ background: white;
+ -moz-box-shadow: inset 0 0 6px rgba(0,0,0,.15);
+ -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.15);
+ box-shadow: inset 0 0 6px rgba(0,0,0,.15);
+}
+
+body.test-dashboard ul {
+ margin: .25em 0;
+ padding: 0.2em 0 0 0;
+}
+
+body.test-dashboard ul ul li {
+ display: block;
+ margin: 0 0 1px .5em;
+ padding: 0.25em 0 0 0.75em;
+ border: 1px solid rgba(0,0,0,0.05);
+ border-width: 0 0 0 1px;
+}
+
+body.test-dashboard .test-menu {
+ float: left;
+ padding: .75em 0 1em 1%;
+ width: 20%;
+ background: #f6f6f6;
+ font-size: .85em;
+}
+
+.test-dashboard .test-menu > ul {
+ margin-top: 0;
+ padding-top: 1px;
+}
+.test-dashboard .test-menu ul li ul {
+ margin-top: .1em;
+}
+.test-dashboard .test-menu li:hover {
+}
+.test-dashboard .test-menu li > ul {
+ display: block;
+}
+.test-dashboard .test-menu li:hover > ul {
+ display: block;
+}
+ul.menu, ul.menu ul {
+ list-style: none;
+}
+ul.menu a {
+ color: #666;
+ text-decoration: none;
+ display: block;
+}
+
+ul.menu a:hover, ul.menu a:active, ul.menu a.menu-folder:hover {
+ color: black;
+}
+
+ul.menu a.menu-folder {
+ color: #333;
+ font-weight: bold;
+ text-decoration: none;
+ font-size: 1.2em;
+}
+
+
+a.test-all {
+ display: block;
+ float: left;
+ font-size: 1.5em;
+ text-align: center;
+ text-decoration: none;
+ padding: .75em 0;
+ width: 21%;
+ color: #666;
+ background: #e6e6e6;
+}
+a.test-all:hover {
+ background: #60AF12;
+ -moz-box-shadow: inset 0 0 12px rgba(0,0,0,.25);
+ -webkit-box-shadow: inset 0 0 12px rgba(0,0,0,.25);
+ box-shadow: inset 0 0 12px rgba(0,0,0,.25);
+ color: white !important;
+ text-shadow: 0px 0px 6px rgba(0,0,0,.5);
+}
+
+ul.menu a {
+ display: block;
+ padding: 0.1em 0;
+}
+
+ul.menu a:before, a.menu-folder:before, ul.metrics li:before {
+ display: inline !important;
+ float: none !important;
+ padding: 0 0.5em 0 0;
+ content: '\25B4';
+ font-weight: normal;
+ color: rgba(0,0,0,.1);
+}
+a.menu-folder:before {
+ padding: 0 !important;
+ content: '\25B2' !important;
+}
+ul.menu a:hover:before, a.menu-folder:hover:before, ul.metrics li:hover:before {
+ color: #60AF12;
}
/*--- Benchmarking ---*/
table.metrics {
- border: 0;
- border-top: 1px solid #ccc;
+ border: 1px solid #e6e6e6;
+ -moz-box-shadow: 0 0 6px rgba(0,0,0,.15);
+ -webkit-box-shadow: 0 0 6px rgba(0,0,0,.15);
+ box-shadow: 0 0 6px rgba(0,0,0,.15);
+}
+table.metrics {
+ border-collapse: collapse;
+}
+table.metrics th {
+ padding: .5em 1em;
+ font-size: 1.1em;
+ color: black;
+ background: #e6e6e6;
+ font-weight: normal;
+}
+table.metrics th, table.metrics td {
+ border-bottom: 1px solid rgba(0,0,0,.05);
}
td.metric-name {
text-align: left;
white-space: nowrap;
padding: 6px 8px;
- background: #f4f4f4;
+ background: #e6e6e6;
width: 35%;
}
+tr:hover td.metric-name {
+ background: #f5f5f5;
+}
td.metric {
- border: 0;
- font-family: 'Courier New', Courier;
+ font-family: 'Andale Mono', Monaco, Courier, monospace !important;
font-weight: bold;
padding: 6px 8px;
text-align: right;
width: 65%;
+ background: #f5f5f5;
+}
+tr:hover td.metric {
+ background: white;
}
ul.classes, ul.files {
list-style-type: none;
- font-family: 'Andale Mono';
+ font-family: 'Andale Mono', Monaco, Courier, monospace !important;
+}
+
+ul.metrics {
+ list-style-type: none;
+ padding: .5em !important;
+}
+
+ul.metrics li {
+ padding: .25em;
}
/*--- Test Results ---*/
div.test-result {
- margin: 0 0 15px 0;
- padding: 8px 10px;
+ clear: both;
+ margin: 1em 0 .5em;
+ padding: .75em 1em .55em;
color: #FFFFFF;
- border: 2px solid #000000;
- font-family: Helvetica, Arial, sans-serif;
- font-weight: bold;
- font-size: 16px;
+ background: #666;
+ border: 1px solid #000000;
+ border-width: 0 0 4px 0;
+ font-size: 1.15em;
+ -moz-box-shadow: inset 0 0 12px rgba(0,0,0,.25);
+ -webkit-box-shadow: inset 0 0 12px rgba(0,0,0,.25);
+ box-shadow: inset 0 0 12px rgba(0,0,0,.25);
+ text-shadow: 0px 0px 6px rgba(0,0,0,.5);
}
div.test-result-success {
- background-color: #33CC66;
- border-color: #009933;
+ background-color: #62B212;
+ border-color: #467F0D;
}
div.test-result-fail {
- background-color: #CC0033;
- border-color: #990000;
+ background-color: #CC1414;
+ border-color: #7F0D0D;
+}
+
+div.test-result-exception {
+ background-color: #E58F16;
+ border-color: #995F0F;
+}
+div.test-result-notice {
+ background-color: #E5D416;
+ border-color: #B2A511;
+}
+
+div.test-result code {
+ background: rgba(0,0,0,.25);
+ color: rgba(255,255,255,.85);
+ border: none;
+ font-size: .75em;
+ padding: .1em .5em !important;
+ text-shadow: none;
}
div.test-assert, div.test-exception, div.test-skip {
- margin: 4px 0;
- padding: 4px 8px;
+ margin: 1px 0 0 0;
+ padding: .5em 0 .5em 1em;
color: #000000;
- border: 1px solid #000000;
- font-family: 'Monaco', 'Andale Mono', Helvetica, Arial, sans-serif;
- line-height: 20px;
- font-size: 12px;
+ font-family: 'Andale Mono', Monaco, Courier, monospace !important;
+ font-size: 1em;
+ line-height: 1.5em;
+ position: relative;
+ border: 1px solid rgba(0,0,0,.05);
+ border-width:0 0 1px 8px;
}
div.test-assert-passed {
- border-color: #339966;
- background-color: #D0F9E0;
+ border-left-color: #D0F9E0;
+}
+
+div.test-assert-failed {
+ color: #7F0D0D;
+ border-left-color: #CC1414;
}
-div.test-assert-failed, div.test-exception {
- border-color: #993366;
- background-color: #F9D0E0;
+div.test-exception {
+ color: #995F0F;
+ border-left-color: #E58F16;
}
-div.test-assert span.content, div.test-exception span.content, div.test-exception span.trace {
+div.test-skip {
+ background-color: #fafafa;
+ color: #666;
+}
+
+.test-assert span.content,
+.test-exception span.content,
+.test-skip span.content,
+.test-assert span.trace,
+.test-exception span.trace,
+.test-skip span.trace {
display: block;
clear: both;
white-space: pre;
+ color: #777;
+ font-size: .9em;
+ padding: .5em 1em;
+ margin: .5em 0;
+ background: #fafafa;
+ border: 1px solid rgba(0,0,0,.1);
+ -moz-box-shadow: inset 0 0 6px rgba(0,0,0,.15);
+ -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.15);
+ box-shadow: inset 0 0 6px rgba(0,0,0,.15);
}
-div.test-exception span.content {
- font-style: italic;
+.test-assert span.trace {
+ padding: 0 .5em;
+ margin: .25em 0 .25em .5em;
+ background: #FAFAFA;
}
div.test-exception span.trace {
- padding: 0 0 0 5px;
- margin: 2px 0 2px 3px;
- border-left: 1px solid #C09090;
-}
-
-div.test-skip {
- border-color: #000000;
- background-color: #CCC;
+ font-style: italic;
}
div.test-skip span.content {
- display: block;
- font-style: italic;
+ color: #999;
+ padding: 0 1em;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ border: none;
}
/*--- SQL Dumps ---*/
.lithium-sql-log table {
@@ -146,21 +396,31 @@ pre {
pre.lithium-debug {
background: #ffcc00;
- font-size: 120%;
- line-height: 140%;
+ font-size: 1.2em;
+ line-height: 1.5em;
margin-top: 1em;
overflow: auto;
position: relative;
}
+div.lithium-exception-class, div.lithium-exception-location {
+ font-weight: bold;
+}
+
+div.lithium-exception-message {
+ color: #000;
+ background: #f0f0f0;
+ padding: 1em;
+}
+
div.lithium-stack-trace {
background: #fff;
border: 4px dotted #ffcc00;
color: #333;
margin: 0px;
padding: 6px;
- font-size: 120%;
- line-height: 140%;
+ font-size: 1.2em;
+ line-height: 1.5em;
overflow: auto;
position: relative;
}
@@ -181,8 +441,8 @@ div.lithium-stack-trace pre, div.lithium-code-dump pre {
div.lithium-code-dump pre, div.lithium-code-dump pre code {
clear: both;
- font-size: 12px;
- line-height: 15px;
+ font-size: 1em;
+ line-height: 1.5em;
margin: 4px 2px;
padding: 4px;
overflow: auto;
@@ -190,81 +450,115 @@ div.lithium-code-dump pre, div.lithium-code-dump pre code {
div.lithium-code-dump span.code-highlight {
background-color: #ff0;
- padding: 4px;
}
/*--- Code Coverage Analysis ---*/
span.filters {
+ display: block;
float: right;
- margin-top: -17px;
+ margin: 1em 0 .5em 0;
+}
+span.filters a {
+ display: block;
+ float: left;
+ padding: .5em 1em;
+ margin-left: .25em;
+ text-decoration:none;
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+ background: #e6e6e6;
+ color: #666;
+}
+span.filters a:hover, span.filters a.active {
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+ color: black;
+ background: #f5f5f5;
+}
+
+span.filters a.active {
+ background: #62B212;
+ text-shadow: 0px 0px 6px rgba(0,0,0,.5);
+ color: white;
}
-div.code-coverage-results {
+div.code-coverage-results, h4.code-coverage-name {
+ clear: both;
color: #000000;
- font-size: 11px;
- font-family: 'Andale Mono';
- background-color: #F0F0F0;
- border: 1px solid #CCCCCC;
+ font-size: .8em;
+ font-family: 'Andale Mono', Monaco, Courier, monospace !important;
+ background-color: #fafafa;
+ border: 1px solid #e6e6e6;
+ border: 1px solid rgba(0,0,0,.1);
+ -moz-box-shadow: 0 0 6px rgba(0,0,0,.15);
+ -webkit-box-shadow: 0 0 6px rgba(0,0,0,.15);
+ box-shadow: 0 0 6px rgba(0,0,0,.15);
}
h4.coverage {
- color: #000000;
- background-color: #FFFFFF;
- font-family: Helvetica, Arial;
- font-weight: bold;
- margin: 6px 0 3px 3px;
+ color: #454545;
+ margin: 2em 0 .5em;
+ font-weight: normal;
padding: 0;
}
-div.code-coverage-results h4.name {
- color: #666;
- background-color: #F0F0F0;
- border-bottom: 1px solid #999;
- padding: 3px 0;
- font-size: 12px;
+h4.code-coverage-name {
+ color: #999;
+ background-color: #ECECEC;
+ border-top: none;
+ padding: 0.25em 0.5em;
+ margin: 0 1px 0 0;
+ font-weight: normal;
+ float: right;
}
div.code-coverage-results div.code-line {
display: block;
float: none;
clear: both;
- padding-left: 5px;
- margin-left: 10px;
}
div.code-coverage-results span.content {
display: block;
clear: right;
white-space: pre;
- line-height: 20px;
+ line-height: 1.5em;
+ min-height: 1.5em;
+ background: black;
}
div.code-coverage-results div.uncovered span.content {
- color: #B00;
- background-color: #FEE;
+ color: #FFD8D8;
+ background-color: #260000;
}
div.code-coverage-results div.covered span.content {
- color: #080;
- background-color: #DFD;
+ color: #DDFDCB;
+ background-color: #0C2600;
}
div.code-coverage-results div.ignored span.content {
- color: #aaa;
+ color: rgba(255,255,255,.5);
}
div.code-coverage-results span.line-num {
display: block;
float: left;
- font-family: Helvetica, Arial, sans-serif;
- width: 20px;
- color: #A9A9A9;
- text-align: right;
+ width: 3em;
+ color: #999;
background-color: #ECECEC;
- border-right: 1px solid #DDDDDD;
- padding-right: 2px;
+ text-align: right;
+ border-right: 1px solid #ccc;
+ padding-right: 4px;
margin-right: 5px;
- line-height: 20px;
+ line-height: 1.5em;
+}
+
+div.code-coverage-results .code-line:hover span.line-num {
+ background: #ddd;
+ color: #666;
}
div.code-coverage-results span.line-num strong {
@@ -293,7 +587,7 @@ div.code-coverage-results p.note {
color: #bbb;
padding: 5px;
margin: 5px 0 10px;
- font-size: 10px;
+ font-size: .8em;
}
div.code-coverage-results span.result-bad {
diff --git a/app/webroot/css/lithium.css b/app/webroot/css/lithium.css
index e6fa9f2..7b643a6 100644
--- a/app/webroot/css/lithium.css
+++ b/app/webroot/css/lithium.css
@@ -38,7 +38,7 @@ h6 { font-size: 1em; }
p { margin-bottom: 1em; }
strong { font-weight: bold; }
em { font-style: italic; }
-a { text-decoration: none; color: #666; border-bottom: 1px dashed #00bbff; }
+a { text-decoration: none; color: #666; }
a, h1 a, h2 a { text-decoration: none; }
a:hover { color: #00bbff; }
a:visited:hover { color: #ff59ff; }
@@ -52,20 +52,29 @@ pre > code {
border: 1px solid white;
padding: 1em !important; /* remove if base important is removed, too */
overflow: auto;
+ font-size: .9em;
+}
+a code {
+ background: transparent;
+ border: 0;
+}
+a:hover code {
+ color: inherit;
}
code, pre, .fixed {
font-family: Monaco, Courier, monospace !important;
font-weight: normal;
- font-size: .85em;
+ font-size: 0.85em;
white-space: pre;
}
code {
padding: .2em .25em !important;
- border: 1px solid #f0f0f0;
- background: #fafafa;
+ border: 1px solid #F0F0F0;
+ background: #FAFAFA;
}
pre {
- padding-bottom: 0.5em;
+ background: none;
+ padding: 0 0 .5em 0 !important;
}
/*--- Lists ---*/
@@ -257,7 +266,7 @@ code {
color: #666;
}
table, form, pre > code {
- margin-top: 12px;
+ margin-top: 0px;
margin-bottom: 12px;
}
table, form, pre > code, .shadow {
diff --git a/app/webroot/index.php b/app/webroot/index.php
index 3b79fa0..4b8e981 100644
--- a/app/webroot/index.php
+++ b/app/webroot/index.php
@@ -10,6 +10,10 @@
* Welcome to Lithium! This front-controller file is the gateway to your application. It is
* responsible for intercepting requests, and handing them off to the `Dispatcher` for processing.
*
+ * @see lithium\action\Dispatcher
+*/
+
+/**
* If you're sharing a single Lithium core install or other libraries among multiple
* applications, you may need to manually set things like `LITHIUM_LIBRARY_PATH`. You can do that in
* `config/bootstrap.php`, which is loaded below:
@@ -23,15 +27,16 @@ require dirname(__DIR__) . '/config/bootstrap.php';
* information.
*
* The `Request` is then used by the `Dispatcher` (in conjunction with the `Router`) to determine
- * the correct controller to dispatch to, and the correct response type to render. The response
- * information is then encapsulated in a `Response` object, which is returned from the controller
- * to the `Dispatcher`, and finally echoed below. Echoing a `Response` object causes its headers to
- * be written, and its response body to be written in a buffer loop.
+ * the correct `Controller` object to dispatch to, and the correct response type to render. The
+ * response information is then encapsulated in a `Response` object, which is returned from the
+ * controller to the `Dispatcher`, and finally echoed below. Echoing a `Response` object causes its
+ * headers to be written, and its response body to be written in a buffer loop.
*
* @see lithium\action\Request
* @see lithium\action\Response
* @see lithium\action\Dispatcher
* @see lithium\net\http\Router
+ * @see lithium\action\Controller
*/
echo lithium\action\Dispatcher::run(new lithium\action\Request());
diff --git a/libraries/_source/empty b/libraries/_source/empty
new file mode 100644
index 0000000..e69de29
diff --git a/libraries/lithium/action/Controller.php b/libraries/lithium/action/Controller.php
index 6f0e36b..7d6ecc2 100644
--- a/libraries/lithium/action/Controller.php
+++ b/libraries/lithium/action/Controller.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\action;
-use \Exception;
+use lithium\util\Inflector;
+use lithium\action\DispatchException;
/**
* The `Controller` class is the fundamental building block of your application's request/response
@@ -20,7 +21,7 @@ use \Exception;
* classes. Each action has a specific responsibility, such as listing a set of objects, updating an
* object, or deleting an object.
*
- * A controller object is instanciated by the `Dispatcher` (`lithium\action\Dispatcher`), and is
+ * A controller object is instantiated by the `Dispatcher` (`lithium\action\Dispatcher`), and is
* given an instance of the `lithium\action\Request` class, which contains all necessary request
* state, including routing information, `GET` & `POST` data, and server variables. The controller
* is then invoked (using PHP's magic `__invoke()` syntax), and the proper action is called,
@@ -62,41 +63,47 @@ class Controller extends \lithium\core\Object {
/**
* Lists the rendering control options for responses generated by this controller.
*
- * The `'type'` key is the content type that will be rendered by default, unless another is
- * explicitly specified (defaults to `'html'`).
+ * - The `'type'` key is the content type that will be rendered by default, unless another is
+ * explicitly specified (defaults to `'html'`).
+ * - The `'data'` key contains an associative array of variables to be sent to the view,
+ * including any variables created in `set()`, or if an action returns any variables (as an
+ * associative array).
+ * - When an action is invoked, it will by default attempt to render a response, set the
+ * `'auto'` key to `false` to prevent this behavior.
+ * - If you manually call `render()` within an action, the `'hasRendered'` key stores this
+ * state, so that responses are not rendered multiple times, either manually or automatically.
+ * - The `'layout'` key specifies the name of the layout to be used (defaults to `'default'`).
+ * Typically, layout files are looked up as
+ * `<app-path>/views/layouts/<layout-name>.<type>.php`. Based on the default settings, the
+ * actual path would be `app/views/layouts/default.html.php`.
+ * - Though typically introspected from the action that is executed, the `'template'` key can be
+ * manually specified. This sets the template to be rendered, and is looked up (by default) as
+ * `<app-path>/views/<controller>/<action>.<type>.php`, i.e.:
+ * `app/views/posts/index.html.php`.
+ * - To enable automatic content-type negotiation (i.e. determining the content type of the
+ * response based on the value of the
+ * [HTTP Accept header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)), set the
+ * `'negotiate'` flag to `true`. Otherwise, the response will only be based on the `type`
+ * parameter of the request object (defaulting to `'html'` if no type is present in the
+ * `Request` parameters).
*
- * The `'data'` key contains an associative array of variables to be sent to the view, including
- * any variables created in `set()`, or if an action returns any variables (as an associative
- * array).
- *
- * When an action is invoked, it will by default attempt to render a response, set the `'auto'`
- * key to `false` to prevent this behavior.
- *
- * If you manually call `render()` within an action, the `'hasRendered'` key stores this state,
- * so that responses are not rendered multiple times, either manually or automatically.
- *
- * The `'layout'` key specifies the name of the layout to be used (defaults to `'default'`).
- * Typically, layout files are looked up as `<app-path>/views/layouts/<layout-name>.<type>.php`.
- * Based on the default settings, the actual path would be `app/views/layouts/default.html.php`.
- *
- * Though typically introspected from the action that is executed, the `'template'` key can be
- * manually specified. This sets the template to be rendered, and is looked up (by default) as
- * `<app-path>/views/<controller>/<action>.<type>.php`, i.e.: `app/views/posts/index.html.php`.
- *
- * To change the inner-workings of these settings (template paths, default render settings for
- * individual content types), see the `lithium\net\http\Media` class.
+ * Keep in mind that most of these settings may be passed to `Controller::render()` as well. To
+ * change how these settings operate (i.e. template paths, default render settings for
+ * individual media types), see the `Media` class.
*
* @var array
+ * @see lithium\action\Controller::render()
* @see lithium\net\http\Media::type()
* @see lithium\net\http\Media::render()
*/
protected $_render = array(
- 'type' => 'html',
+ 'type' => null,
'data' => array(),
'auto' => true,
'layout' => 'default',
'template' => null,
- 'hasRendered' => false
+ 'hasRendered' => false,
+ 'negotiate' => false
);
/**
@@ -106,11 +113,16 @@ class Controller extends \lithium\core\Object {
* @var array
*/
protected $_classes = array(
- 'media' => '\lithium\net\http\Media',
- 'router' => '\lithium\net\http\Router',
- 'response' => '\lithium\action\Response'
+ 'media' => 'lithium\net\http\Media',
+ 'router' => 'lithium\net\http\Router',
+ 'response' => 'lithium\action\Response'
);
+ /**
+ * Auto configuration properties.
+ *
+ * @var array
+ */
protected $_autoConfig = array('render' => 'merge', 'classes' => 'merge');
public function __construct(array $config = array()) {
@@ -123,13 +135,16 @@ class Controller extends \lithium\core\Object {
protected function _init() {
parent::_init();
$this->request = $this->request ?: $this->_config['request'];
+ $this->response = $this->_instance('response', $this->_config['response']);
- if ($this->request) {
- $this->_render['type'] = $this->request->type();
+ if (!$this->request || $this->_render['type']) {
+ return;
}
-
- $config = $this->_config['response'] + array('request' => $this->request);
- $this->response = new $this->_classes['response']($config);
+ if ($this->_render['negotiate']) {
+ $this->_render['type'] = $this->request->accepts();
+ return;
+ }
+ $this->_render['type'] = $this->request->type ?: 'html';
}
/**
@@ -151,25 +166,24 @@ class Controller extends \lithium\core\Object {
$dispatchParams = $params['dispatchParams'];
$options = $params['options'];
- $action = $dispatchParams['action'];
+ $action = isset($dispatchParams['action']) ? $dispatchParams['action'] : 'index';
$args = isset($dispatchParams['args']) ? $dispatchParams['args'] : array();
$result = null;
if (substr($action, 0, 1) == '_' || method_exists(__CLASS__, $action)) {
- throw new Exception('Private method!');
+ throw new DispatchException('Attempted to invoke a private method.');
}
- $render['template'] = $render['template'] ?: $action;
-
- try {
- $result = $self->invokeMethod($action, $args);
- } catch (Exception $e) {
- throw $e;
+ if (!method_exists($self, $action)) {
+ throw new DispatchException("Action `{$action}` not found.");
}
+ $render['template'] = $render['template'] ?: $action;
- if ($result) {
+ if ($result = $self->invokeMethod($action, $args)) {
if (is_string($result)) {
$self->render(array('text' => $result));
- } elseif (is_array($result)) {
+ return $self->response;
+ }
+ if (is_array($result)) {
$self->set($result);
}
}
@@ -193,43 +207,47 @@ class Controller extends \lithium\core\Object {
/**
* Uses results (typically coming from a controller action) to generate content and headers for
- * a Response object.
+ * a `Response` object.
*
- * @param string|array $options A string template name (see the 'template' option below), or an
- * array of options, as follows:
+ * @see lithium\action\Controller::$_render
+ * @param array $options An array of options, as follows:
* - `'data'`: An associative array of variables to be assigned to the template. These
* are merged on top of any variables set in `Controller::set()`.
* - `'head'`: If true, only renders the headers of the response, not the body. Defaults
- * to false.
+ * to `false`.
* - `'template'`: The name of a template, which usually matches the name of the action.
* By default, this template is looked for in the views directory of the current
* controller, i.e. given a `PostsController` object, if template is set to `'view'`,
* the template path would be `views/posts/view.html.php`. Defaults to the name of the
* action being rendered.
+ *
+ * The options specified here are merged with the values in the `Controller::$_render`
+ * property. You may refer to it for other options accepted by this method.
* @return void
*/
- public function render($options = array()) {
- if (is_string($options)) {
- $options = array('template' => $options);
- }
- $defaults = array(
- 'status' => null, 'location' => false, 'data' => array(), 'head' => false
- );
- $options += $defaults;
+ public function render(array $options = array()) {
$media = $this->_classes['media'];
+ $class = get_class($this);
+ $name = preg_replace('/Controller$/', '', substr($class, strrpos($class, '\\') + 1));
+ $key = key($options);
- if (!empty($options['data'])) {
+ if (isset($options['data'])) {
$this->set($options['data']);
unset($options['data']);
}
- $options = $options + $this->_render;
- $type = key($options);
- $types = array_flip($media::types());
+ $defaults = array(
+ 'status' => null,
+ 'location' => false,
+ 'data' => null,
+ 'head' => false,
+ 'controller' => Inflector::underscore($name),
+ );
+ $options += $this->_render + $defaults;
- if (isset($types[$type])) {
- $options['type'] = $type;
- $this->set(current($options));
- unset($options[$type]);
+ if ($key && $media::type($key)) {
+ $options['type'] = $key;
+ $this->set($options[$key]);
+ unset($options[$key]);
}
$this->_render['hasRendered'] = true;
@@ -246,21 +264,35 @@ class Controller extends \lithium\core\Object {
}
/**
- * Creates a redirect response.
+ * Creates a redirect response by calling `render()` and providing a `'location'` parameter.
*
- * @param mixed $url
- * @param array $options
- * @return void
+ * @see lithium\net\http\Router::match()
+ * @see lithium\action\Controller::$response
+ * @param mixed $url The location to redirect to, provided as a string relative to the root of
+ * the application, a fully-qualified URL, or an array of routing parameters to be
+ * resolved to a URL. Post-processed by `Router::match()`.
+ * @param array $options Options when performing the redirect. Available options include:
+ * - `'status'` _integer_: The HTTP status code associated with the redirect.
+ * Defaults to `302`.
+ * - `'head'` _boolean_: Determines whether only headers are returned with the
+ * response. Defaults to `true`, in which case only headers and no body are
+ * returned. Set to `false` to render a body as well.
+ * - `'exit'` _boolean_: Exit immediately after rendering. Defaults to `false`.
+ * Because `redirect()` does not exit by default, you should always prefix calls
+ * with a `return` statement, so that the action is always immedately exited.
+ * @return object Returns the instance of the `Response` object associated with this controller.
* @filter This method can be filtered.
*/
public function redirect($url, array $options = array()) {
$router = $this->_classes['router'];
- $defaults = array('location' => null, 'status' => 302, 'head' => true, 'exit' => true);
+ $defaults = array('location' => null, 'status' => 302, 'head' => true, 'exit' => false);
$options += $defaults;
- $options['location'] = $options['location'] ?: $router::match($url, $this->request);
+ $params = compact('url', 'options');
- $this->_filter(__METHOD__, compact('options'), function($self, $params, $chain) {
- $self->render($params['options']);
+ $this->_filter(__METHOD__, $params, function($self, $params) use ($router) {
+ $options = $params['options'];
+ $location = $options['location'] ?: $router::match($params['url'], $self->request);
+ $self->render(compact('location') + $options);
});
if ($options['exit']) {
diff --git a/libraries/lithium/action/DispatchException.php b/libraries/lithium/action/DispatchException.php
new file mode 100644
index 0000000..337bc99
--- /dev/null
+++ b/libraries/lithium/action/DispatchException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\action;
+
+/**
+ * This exception covers a range of scenarios that generally revolve around attempting to dispatch
+ * to something which cannot handle a request, i.e. a controller which can't be found, objects
+ * which aren't callable, or un-routable (private) controller methods.
+ */
+class DispatchException extends \RuntimeException {
+
+ protected $code = 404;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/action/Dispatcher.php b/libraries/lithium/action/Dispatcher.php
index 8262f4b..457c6e9 100644
--- a/libraries/lithium/action/Dispatcher.php
+++ b/libraries/lithium/action/Dispatcher.php
@@ -2,16 +2,17 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\action;
-use \Exception;
-use \lithium\util\String;
-use \lithium\util\Inflector;
-use \lithium\core\Libraries;
+use lithium\util\String;
+use lithium\util\Inflector;
+use lithium\core\Libraries;
+use lithium\action\DispatchException;
+use lithium\core\ClassNotFoundException;
/**
* `Dispatcher` is the outermost layer of the framework, responsible for both receiving the initial
@@ -40,17 +41,17 @@ class Dispatcher extends \lithium\core\StaticObject {
* @var array
*/
protected static $_classes = array(
- 'router' => '\lithium\net\http\Router'
+ 'router' => 'lithium\net\http\Router'
);
/**
* Contains pre-process format strings for changing Dispatcher's behavior based on 'rules'.
*
* Each key in the array represents a 'rule'; if a key that matches the rule is present (and
- * not empty) in a route, (i.e. the result of `lithium\net\http\Router::parse()`) then the rule's
- * value will be applied to the route before it is dispatched. When applying a rule, any array
- * elements array elements of the flag which are present in the route will be modified using a
- * `lithium\util\String::insert()`-formatted string.
+ * not empty) in a route, (i.e. the result of `lithium\net\http\Router::parse()`) then the
+ * rule's value will be applied to the route before it is dispatched. When applying a rule, any
+ * array elements array elements of the flag which are present in the route will be modified
+ * using a `lithium\util\String::insert()`-formatted string.
*
* For example, to implement action prefixes (i.e. `admin_index()`), set a rule named 'admin',
* with a value array containing a modifier key for the `action` element of a route, i.e.:
@@ -75,7 +76,7 @@ class Dispatcher extends \lithium\core\StaticObject {
* configuration, otherwise returns `null`.
*/
public static function config(array $config = array()) {
- if (empty($config)) {
+ if (!$config) {
return array('rules' => static::$_rules);
}
@@ -109,56 +110,16 @@ class Dispatcher extends \lithium\core\StaticObject {
$request = $params['request'];
$options = $params['options'];
- $request->params = $router::parse($request);
- $params = $self::invokeMethod('_applyRules', array($request->params));
-
- if (!$params) {
- throw new Exception('Could not route request');
- }
-
- $callable = $self::invokeMethod('_callable', array($request, $params, $options));
- return $self::invokeMethod('_call', array($callable, $request, $params));
- });
- }
-
- /**
- * Accepts parameters generated by the `Router` class in `Dispatcher::run()`, and produces a
- * callable controller object. By default, this method uses the `'controller'` path lookup
- * configuration in `Libraries::locate()` to return a callable object.
- *
- * @param object $request The instance of the `Request` class either passed into or generated by
- * `Dispatcher::run()`.
- * @param array $params The parameter array generated by routing the request.
- * @param array $options Not currently implemented.
- * @return object Returns a callable object which the request will be routed to.
- */
- protected static function _callable($request, $params, $options) {
- $params = compact('request', 'params', 'options');
- return static::_filter(__FUNCTION__, $params, function($self, $params, $chain) {
- extract($params, EXTR_OVERWRITE);
- $library = '';
-
- if (strpos($params['controller'], '.')) {
- list($library, $params['controller']) = explode('.', $params['controller']);
- $library .= '.';
- }
- $controller = $library . Inflector::camelize($params['controller']);
- $class = Libraries::locate('controllers', $controller);
-
- if (class_exists($class)) {
- return new $class(compact('request'));
+ if (($result = $router::process($request)) instanceof Response) {
+ return $result;
}
- throw new Exception("Controller {$controller} not found");
- });
- }
+ $params = $self::applyRules($result->params);
- protected static function _call($callable, $request, $params) {
- $params = compact('callable', 'request', 'params');
- return static::_filter(__FUNCTION__, $params, function($self, $params, $chain) {
- if (is_callable($callable = $params['callable'])) {
- return $callable($params['request'], $params['params']);
+ if (!$params) {
+ throw new DispatchException('Could not route request.');
}
- throw new Exception('Result not callable');
+ $callable = $self::invokeMethod('_callable', array($result, $params, $options));
+ return $self::invokeMethod('_call', array($callable, $result, $params));
});
}
@@ -171,28 +132,82 @@ class Dispatcher extends \lithium\core\StaticObject {
* @param array $params An array of route parameters to which rules will be applied.
* @return array Returns the `$params` array with formatting rules applied to array values.
*/
- protected static function _applyRules($params) {
+ public static function applyRules(&$params) {
$result = array();
+ $values = array();
if (!$params) {
return false;
}
+ if (isset($params['controller']) && is_string($params['controller'])) {
+ $controller = $params['controller'];
+
+ if (strpos($controller, '.') !== false) {
+ list($library, $controller) = explode('.', $controller);
+ $controller = $library . '.' . Inflector::camelize($controller);
+ $params += compact('library');
+ } elseif (strpos($controller, '\\') === false) {
+ $controller = Inflector::camelize($controller);
+
+ if (isset($params['library'])) {
+ $controller = "{$params['library']}.{$controller}";
+ }
+ }
+ $values = compact('controller');
+ }
+ $values += $params;
+
foreach (static::$_rules as $rule => $value) {
foreach ($value as $k => $v) {
- if (!empty($params[$rule])) {
- $result[$k] = String::insert($v, $params);
+ if (isset($values[$rule])) {
+ $result[$k] = String::insert($v, $values);
}
-
$match = preg_replace('/\{:\w+\}/', '@', $v);
$match = preg_replace('/@/', '.+', preg_quote($match, '/'));
- if (preg_match('/' . $match . '/i', $params[$k])) {
+ if (preg_match('/' . $match . '/i', $values[$k])) {
return false;
}
}
}
- return $result + array_diff_key($params, $result);
+ return $result + $values;
+ }
+
+ /**
+ * Accepts parameters generated by the `Router` class in `Dispatcher::run()`, and produces a
+ * callable controller object. By default, this method uses the `'controller'` path lookup
+ * configuration in `Libraries::locate()` to return a callable object.
+ *
+ * @param object $request The instance of the `Request` class either passed into or generated by
+ * `Dispatcher::run()`.
+ * @param array $params The parameter array generated by routing the request.
+ * @param array $options Not currently implemented.
+ * @return object Returns a callable object which the request will be routed to.
+ */
+ protected static function _callable($request, $params, $options) {
+ $params = compact('request', 'params', 'options');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $options = array('request' => $params['request']) + $params['options'];
+ $controller = $params['params']['controller'];
+
+ try {
+ return Libraries::instance('controllers', $controller, $options);
+ } catch (ClassNotFoundException $e) {
+ throw new DispatchException("Controller `{$controller}` not found.", null, $e);
+ }
+ });
+ }
+
+ protected static function _call($callable, $request, $params) {
+ $params = compact('callable', 'request', 'params');
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ if (is_callable($callable = $params['callable'])) {
+ return $callable($params['request'], $params['params']);
+ }
+ throw new DispatchException('Result not callable.');
+ });
}
}
diff --git a/libraries/lithium/action/Request.php b/libraries/lithium/action/Request.php
index 4518c34..458a7c9 100644
--- a/libraries/lithium/action/Request.php
+++ b/libraries/lithium/action/Request.php
@@ -2,18 +2,19 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\action;
-use \lithium\util\Validator;
+use lithium\util\Set;
+use lithium\util\Validator;
/**
- * A `Request` object is passed into or instantiated by the `Dispatcher`, and is responsible for
- * identifying and storing all the information about an HTTP request made to an application,
- * including status, headers, and any GET, POST or PUT data, as well as any data returned from the
+ * A `Request` object is passed into the `Dispatcher`, and is responsible for identifying and
+ * storing all the information about an HTTP request made to an application, including status,
+ * headers, and any GET, POST or PUT data, as well as any data returned from the
* `Router`, after the `Request` object has been matched against a `Route`. Includes a property
* accessor method (`__get()`) which allows any parameters returned from routing to be accessed as
* properties of the `Request` object.
@@ -24,38 +25,45 @@ use \lithium\util\Validator;
* @see lithium\net\http\Route
* @see lithium\action\Request::__get()
*/
-class Request extends \lithium\core\Object {
+class Request extends \lithium\net\http\Request {
/**
- * current url of request
+ * Current url of request.
*
* @var string
*/
public $url = null;
/**
- * params for request
+ * Params for request.
*
* @var array
*/
public $params = array();
/**
- * POST data
+ * Route parameters that should persist when generating URLs in this request context.
+ *
+ * @var array
+ */
+ public $persist = array();
+
+ /**
+ * POST data.
*
* @var data
*/
public $data = array();
/**
- * GET data
+ * GET data.
*
* @var string
*/
public $query = array();
/**
- * base path
+ * Base path.
*
* @var string
*/
@@ -70,23 +78,25 @@ class Request extends \lithium\core\Object {
protected $_env = array();
/**
- * request type
+ * Classes used by `Request`.
*
- * @var string
+ * @var array
*/
- protected $_type = null;
+ protected $_classes = array('media' => 'lithium\net\http\Media');
/**
- * classes used
+ * If POST / PUT data is coming from an input stream (rather than `$_POST`), this specified
+ * where to read it from.
*
- * @var array
+ * @var stream
*/
- protected $_classes = array('media' => '\lithium\net\http\Media');
+ protected $_stream = null;
/**
- * options used to detect request type
+ * Options used to detect request type.
*
- * @var string
+ * @see lithium\action\Request::detect()
+ * @var array
*/
protected $_detectors = array(
'mobile' => array('HTTP_USER_AGENT', null),
@@ -102,22 +112,21 @@ class Request extends \lithium\core\Object {
);
/**
- * Content-types accepted by the client. If extension parsing is enabled in the Router, and an
- * extension is detected, the corresponding content-type will be used as the overriding primary
- * content-type accepted.
+ * Auto configuration properties.
*
* @var array
*/
- protected $_acceptTypes = array();
+ protected $_autoConfig = array(
+ 'classes' => 'merge', 'env', 'detectors' => 'merge', 'base', 'type', 'stream'
+ );
/**
- * Auto configuration properties.
+ * Contains an array of content-types, sorted by quality (the priority which the browser
+ * requests each type).
*
* @var array
*/
- protected $_autoConfig = array(
- 'classes' => 'merge', 'env' => 'merge', 'detectors' => 'merge', 'base', 'type'
- );
+ protected $_acceptContent = array();
/**
* Pulls request data from superglobals.
@@ -126,6 +135,7 @@ class Request extends \lithium\core\Object {
*/
protected function _init() {
parent::_init();
+
$mobile = array(
'iPhone', 'MIDP', 'AvantGo', 'BlackBerry', 'J2ME', 'Opera Mini', 'DoCoMo', 'NetFront',
'Nokia', 'PalmOS', 'PalmSource', 'portalmmm', 'Plucker', 'ReqwirelessWeb', 'iPod',
@@ -138,7 +148,7 @@ class Request extends \lithium\core\Object {
$this->_env += (array) $_SERVER + (array) $_ENV + array('REQUEST_METHOD' => 'GET');
$envs = array('isapi' => 'IIS', 'cgi' => 'CGI', 'cgi-fcgi' => 'CGI');
$this->_env['PLATFORM'] = isset($envs[PHP_SAPI]) ? $envs[PHP_SAPI] : null;
- $this->_base = $this->_base ?: $this->_base();
+ $this->_base = isset($this->_base) ? $this->_base : $this->_base();
$this->url = '/';
if (isset($this->_config['url'])) {
@@ -148,8 +158,6 @@ class Request extends \lithium\core\Object {
unset($_GET['url']);
}
- $this->query = $this->data = array();
-
if (!empty($this->_config['query'])) {
$this->query = $this->_config['query'];
}
@@ -159,12 +167,11 @@ class Request extends \lithium\core\Object {
if (!empty($this->_config['data'])) {
$this->data = $this->_config['data'];
- }
- if (isset($_POST)) {
+ } elseif (isset($_POST)) {
$this->data += $_POST;
}
- if (!empty($this->data['_method'])) {
+ if (isset($this->data['_method'])) {
$this->_env['HTTP_X_HTTP_METHOD_OVERRIDE'] = strtoupper($this->data['_method']);
unset($this->data['_method']);
}
@@ -176,16 +183,17 @@ class Request extends \lithium\core\Object {
$method = strtoupper($this->_env['REQUEST_METHOD']);
if (($method == 'POST' || $method == 'PUT') && !$this->data) {
- $media = $this->_classes['media'];
- $key = 'CONTENT_TYPE';
-
- if (isset($this->_env[$key]) && $type = $media::type($this->_env[$key])) {
- $this->data = $media::decode($type, file_get_contents('php://input'));
+ if (($type = $this->type()) && $type !== 'html') {
+ $this->_stream = $this->_stream ?: fopen('php://input', 'r');
+ $media = $this->_classes['media'];
+ $this->data = (array) $media::decode($type, stream_get_contents($this->_stream));
+ fclose($this->_stream);
}
}
if (isset($_FILES) && $_FILES) {
$result = array();
+
$normalize = function($key, $value) use ($result, &$normalize){
foreach ($value as $param => $content) {
foreach ($content as $num => $val) {
@@ -215,7 +223,7 @@ class Request extends \lithium\core\Object {
}
}
}
- $this->data = (array) $this->data + $result;
+ $this->data = Set::merge((array) $this->data, $result);
}
}
@@ -234,6 +242,19 @@ class Request extends \lithium\core\Object {
}
/**
+ * Allows request parameters to be checked using short-hand notation. See the `__get()` method
+ * for more details.
+ *
+ * @see lithium\action\Request::__get()
+ * @param string $name The name of the request parameter to check.
+ * @return boolean Returns true if the key in `$name` is set in the `$params` array, otherwise
+ * `false`.
+ */
+ public function __isset($name) {
+ return isset($this->params[$name]);
+ }
+
+ /**
* Queries PHP's environment settings, and provides an abstraction for standardizing expected
* environment values across varying platforms, as well as specify custom environment flags.
*
@@ -242,17 +263,10 @@ class Request extends \lithium\core\Object {
* @todo Refactor to lazy-load environment settings
*/
public function env($key) {
- if ($key == 'base') {
+ if (strtolower($key) == 'base') {
return $this->_base;
}
- if ($key == 'HTTPS') {
- if (isset($this->_env['HTTPS'])) {
- return (!empty($this->_env['HTTPS']) && $this->_env['HTTPS'] !== 'off');
- }
- return (strpos($this->_env['SCRIPT_URI'], 'https://') === 0);
- }
-
if ($key == 'SCRIPT_NAME' && !isset($this->_env['SCRIPT_NAME'])) {
if ($this->_env['PLATFORM'] == 'CGI' || isset($this->_env['SCRIPT_URL'])) {
$key = 'SCRIPT_URL';
@@ -266,16 +280,24 @@ class Request extends \lithium\core\Object {
$val = ($addr = $this->env('HTTP_PC_REMOTE_ADDR')) ? $addr : $val;
}
- if ($val !== null && $val !== false) {
+ if ($val !== null && $val !== false && $key !== 'HTTPS') {
return $val;
}
switch ($key) {
+ case 'HTTPS':
+ if (isset($this->_env['SCRIPT_URI'])) {
+ return (strpos($this->_env['SCRIPT_URI'], 'https://') === 0);
+ }
+ if (isset($this->_env['HTTPS'])) {
+ return (!empty($this->_env['HTTPS']) && $this->_env['HTTPS'] !== 'off');
+ }
+ return false;
case 'SCRIPT_FILENAME':
if ($this->_env['PLATFORM'] == 'IIS') {
return str_replace('\\\\', '\\', $this->env('PATH_TRANSLATED'));
}
- return $this->env('PHP_SELF');
+ return $this->env('DOCUMENT_ROOT') . $this->env('PHP_SELF');
case 'DOCUMENT_ROOT':
$fileName = $this->env('SCRIPT_FILENAME');
$offset = (!strpos($this->env('SCRIPT_NAME'), '.php')) ? 4 : 0;
@@ -294,10 +316,83 @@ class Request extends \lithium\core\Object {
}
/**
- * Get params, data, query or env
+ * Returns information about the type of content that the client is requesting.
*
- * @param string $key data:title, env:base
- * @return void
+ * @see lithium\net\http\Media::negotiate()
+ * @param $type mixed If not specified, returns the media type name that the client prefers,
+ * using content negotiation. If a media type name (string) is passed, returns
+ * `true` or `false`, indicating whether or not that type is accepted by the client
+ * at all. If `true`, returns the raw content types from the `Accept` header,
+ * parsed into an array and sorted by client preference.
+ * @return string Returns a simple type name if the type is registered (i.e. `'json'`), or
+ * a fully-qualified content-type if not (i.e. `'image/jpeg'`), or a boolean or array,
+ * depending on the value of `$type`.
+ */
+ public function accepts($type = null) {
+ if ($type === true) {
+ return $this->_parseAccept();
+ }
+ if (!$type && isset($this->params['type'])) {
+ return $this->params['type'];
+ }
+ $media = $this->_classes['media'];
+ return $media::negotiate($this) ?: 'html';
+ }
+
+ protected function _parseAccept() {
+ if ($this->_acceptContent) {
+ return $this->_acceptContent;
+ }
+ $accept = $this->env('HTTP_ACCEPT');
+ $accept = (preg_match('/[a-z,-]/i', $accept)) ? explode(',', $accept) : array('text/html');
+
+ foreach (array_reverse($accept) as $i => $type) {
+ unset($accept[$i]);
+ list($type, $q) = (explode(';q=', $type, 2) + array($type, 1.0 + $i / 100));
+ $accept[$type] = ($type == '*/*') ? 0.1 : floatval($q);
+ }
+ arsort($accept, SORT_NUMERIC);
+
+ if (isset($accept['application/xhtml+xml']) && $accept['application/xhtml+xml'] >= 1) {
+ unset($accept['application/xml']);
+ }
+ $media = $this->_classes['media'];
+
+ if (isset($this->params['type']) && ($handler = $media::type($this->params['type']))) {
+ if (isset($handler['content'])) {
+ $type = (array) $handler['content'];
+ $accept = array(current($type) => 1) + $accept;
+ }
+ }
+ return $this->_acceptContent = array_keys($accept);
+ }
+
+ /**
+ * This method allows easy extraction of any request data using a prefixed key syntax. By
+ * passing keys in the form of `'prefix:key'`, it is possible to query different information of
+ * various different types, including GET and POST data, and server environment variables. The
+ * full list of prefixes is as follows:
+ *
+ * - `'data'`: Retrieves values from POST data.
+ * - `'params'`: Retrieves query parameters returned from the routing system.
+ * - `'query'`: Retrieves values from GET data.
+ * - `'env'`: Retrieves values from the server or environment, such as `'env:https'`, or custom
+ * environment values, like `'env:base'`. See the `env()` method for more info.
+ * - `'http'`: Retrieves header values (i.e. `'http:accept'`), or the HTTP request method (i.e.
+ * `'http:method'`).
+ *
+ * This method is used in several different places in the framework in order to provide the
+ * ability to act conditionally on different aspects of the request. See `Media::type()` (the
+ * section on content negotiation) and the routing system for more information.
+ *
+ * _Note_: All keys should be _lower-cased_, even when getting HTTP headers.
+ * @see lithium\action\Request::env()
+ * @see lithium\net\http\Media::type()
+ * @see lithium\net\http\Router
+ * @param string $key A prefixed key indiciating what part of the request data the requested
+ * value should come from, and the name of the value to retrieve, in lower case.
+ * @return string Returns the value of a GET, POST, routing or environment variable, or an
+ * HTTP header or method name.
*/
public function get($key) {
list($var, $key) = explode(':', $key);
@@ -305,10 +400,13 @@ class Request extends \lithium\core\Object {
switch (true) {
case in_array($var, array('params', 'data', 'query')):
return isset($this->{$var}[$key]) ? $this->{$var}[$key] : null;
- case ($var == 'env'):
- return $this->env($key);
+ case ($var === 'env'):
+ return $this->env(strtoupper($key));
+ case ($var === 'http' && $key === 'method'):
+ return $this->env('REQUEST_METHOD');
+ case ($var === 'http'):
+ return $this->env('HTTP_' . strtoupper($key));
}
- return null;
}
/**
@@ -320,55 +418,73 @@ class Request extends \lithium\core\Object {
* @return boolean
*/
public function is($flag) {
- $flag = strtolower($flag);
-
- if (!empty($this->_detectors[$flag])) {
- $detector = $this->_detectors[$flag];
+ $media = $this->_classes['media'];
- if (is_array($detector)) {
- list($key, $check) = $detector + array('', '');
- if (is_array($check)) {
- $check = '/' . join('|', $check) . '/i';
- }
- if (Validator::isRegex($check)) {
- return (boolean) preg_match($check, $this->env($key));
- }
- return ($this->env($key) == $check);
- }
- if (is_callable($detector)) {
- return $detector($this);
+ if (!isset($this->_detectors[$flag])) {
+ if (!in_array($flag, $media::types())) {
+ return false;
}
+ return $this->type() == $flag;
+ }
+ $detector = $this->_detectors[$flag];
+
+ if (!is_array($detector) && is_callable($detector)) {
+ return $detector($this);
+ }
+ if (!is_array($detector)) {
return (boolean) $this->env($detector);
}
- return false;
+ list($key, $check) = $detector + array('', '');
+
+ if (is_array($check)) {
+ $check = '/' . join('|', $check) . '/i';
+ }
+ if (Validator::isRegex($check)) {
+ return (boolean) preg_match($check, $this->env($key));
+ }
+ return ($this->env($key) == $check);
}
/**
- * Returns the content type of the response.
+ * Sets/Gets the content type. If `'type'` is null, the method will attempt to determine the
+ * type first, from the params, then from the environment setting
*
+ * @param string $type a full content type i.e. `'application/json'` or simple name `'json'`
* @return string A simple content type name, i.e. `'html'`, `'xml'`, `'json'`, etc., depending
* on the content type of the request.
*/
- public function type() {
- if ($this->_type !== null) {
- return $this->_type;
- }
- if (!empty($this->params['type'])) {
- return $this->_type = $this->params['type'];
- }
- if ($type = $this->env('Content-type')) {
- return $this->_type = $type;
+ public function type($type = null) {
+ if ($type === null) {
+ $type = $this->type ?: $this->env('CONTENT_TYPE');
}
- return $this->_type = 'html';
+ return parent::type($type);
}
/**
- * Creates a 'detector' used with Request::is(). A detector is a boolean check that is created
- * to determine something about a request.
+ * Creates a _detector_ used with `Request::is()`. A detector is a boolean check that is
+ * created to determine something about a request.
+ *
+ * A detector check can be either an exact string match or a regular expression match against a
+ * header or environment variable. A detector check can also be a closure that accepts the
+ * `Request` object instance as a parameter.
+ *
+ * For example, to detect whether a request is from an iPhone, you can do the following:
+ * {{{ embed:lithium\tests\cases\action\RequestTest::testDetect(11-12) }}}
*
* @see lithium\action\Request::is()
- * @param string $flag
- * @param boolean $detector
+ * @param string $flag The name of the detector check. Used in subsequent calls to
+ * `Request::is()`.
+ * @param mixed $detector Detectors can be specified in four different ways:
+ * - The name of an HTTP header or environment variable. If a string, calling the
+ * detector will check that the header or environment variable exists and is set
+ * to a non-empty value.
+ * - A two-element array containing a header/environment variable name, and a value
+ * to match against. The second element of the array must be an exact match to
+ * the header or variable value.
+ * - A two-element array containing a header/environment variable name, and a
+ * regular expression that matches against the value, as in the example above.
+ * - A closure which accepts an instance of the `Request` object and returns a
+ * boolean value.
* @return void
*/
public function detect($flag, $detector = null) {
@@ -385,11 +501,9 @@ class Request extends \lithium\core\Object {
* @param string $default Default URL to use if HTTP_REFERER cannot be read from headers.
* @param boolean $local If true, restrict referring URLs to local server.
* @return string Referring URL.
- * @todo Rewrite me to remove constant dependencies.
*/
- function referer($default = null, $local = false) {
- $ref = $this->env('HTTP_REFERER');
- if (!empty($ref)) {
+ public function referer($default = null, $local = false) {
+ if ($ref = $this->env('HTTP_REFERER')) {
if (!$local) {
return $ref;
}
@@ -401,12 +515,32 @@ class Request extends \lithium\core\Object {
}
/**
+ * Overrides `lithium\net\http\Request::to()` to provide the correct options for generating
+ * URLs. For information about this method, see the parent implementation.
+ *
+ * @see lithium\net\http\Request::to()
+ * @param string $format The format to convert to.
+ * @param array $options Override options.
+ * @return mixed The return value type depends on `$format`.
+ */
+ public function to($format, array $options = array()) {
+ $defaults = array(
+ 'scheme' => $this->env('HTTPS') ? 'https' : 'http',
+ 'host' => $this->env('HTTP_HOST'),
+ 'path' => $this->_base . $this->url,
+ 'query' => $this->query
+ );
+ $options += $defaults;
+ return parent::to($format, $options);
+ }
+
+ /**
* @todo Replace string directory names with configuration.
* @return void
*/
protected function _base() {
$base = str_replace('\\', '/', dirname($this->env('PHP_SELF')));
- return rtrim(str_replace(array('/app/webroot', '/webroot'), '', $base), '/');
+ return rtrim(str_replace(array("/app/webroot", '/webroot'), '', $base), '/');
}
}
diff --git a/libraries/lithium/action/Response.php b/libraries/lithium/action/Response.php
index 78ffce3..72de487 100644
--- a/libraries/lithium/action/Response.php
+++ b/libraries/lithium/action/Response.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\action;
-use \Exception;
+use UnexpectedValueException;
+use BadMethodCallException;
/**
* A `Response` object is typically instantiated automatically by the `Controller`. It is assigned
@@ -23,48 +24,74 @@ use \Exception;
*/
class Response extends \lithium\net\http\Response {
- protected $_config = array();
+ /**
+ * Classes used by Response.
+ *
+ * @var array
+ */
+ protected $_classes = array(
+ 'router' => 'lithium\net\http\Router',
+ 'media' => 'lithium\net\http\Media'
+ );
+
+ protected $_autoConfig = array('classes' => 'merge');
public function __construct(array $config = array()) {
- $defaults = array('buffer' => 8192, 'request' => null);
+ $defaults = array('buffer' => 8192, 'location' => null, 'status' => 0, 'request' => null);
parent::__construct($config + $defaults);
}
protected function _init() {
parent::_init();
-
- if (!empty($this->_config['request']) && is_object($this->_config['request'])) {
- $this->type = $this->_config['request']->type();
+ $config = $this->_config;
+ $this->status($config['status']);
+ unset($this->_config['status']);
+
+ if ($config['location']) {
+ $classes = $this->_classes;
+ $location = $classes['router']::match($config['location'], $config['request']);
+ $this->headers('location', $location);
}
}
/**
- * Content Type.
+ * Disables HTTP caching for web clients and proxies.
*
- * @param string $type
- * @return string
+ * @return void
+ * @deprecated
*/
- public function type($type = null) {
- if ($type !== null) {
- return $this->type = $type;
- }
- return $this->type;
+ public function disableCache() {
+ $message = '`Request::disableCache()` is deprecated. Please use `Request::cache(false)`.';
+ throw new BadMethodCallException($message);
}
/**
- * Disables HTTP caching for web clients and proxies.
+ * Controls how or whether the client browser and web proxies should cache this response.
*
+ * @param mixed $expires This can be a Unix timestamp indicating when the page expires, or a
+ * string indicating the relative time offset that a page should expire, i.e.
+ * `"+5 hours". Finally, `$expires` can be set to `false` to completely disable
+ * browser or proxy caching.
* @return void
*/
- public function disableCache() {
- $this->headers(array(
- 'Expires' => "Mon, 26 Jul 1997 05:00:00 GMT",
- 'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT",
- 'Cache-Control' => array(
- "no-store, no-cache, must-revalidate",
- "post-check=0, pre-check=0"
- ),
- 'Pragma' => 'no-cache'
+ public function cache($expires) {
+ if ($expires === false) {
+ return $this->headers(array(
+ 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
+ 'Cache-Control' => array(
+ 'no-store, no-cache, must-revalidate',
+ 'post-check=0, pre-check=0',
+ 'max-age=0',
+ ),
+ 'Pragma' => 'no-cache',
+ ));
+ }
+ $expires = is_int($expires) ? $expires : strtotime($expires);
+
+ return $this->headers(array(
+ 'Expires' => gmdate('D, d M Y H:i:s', $expires) . ' GMT',
+ 'Cache-Control' => 'max-age=' . ($expires - time()),
+ 'Pragma' => 'cache',
));
}
@@ -80,12 +107,7 @@ class Response extends \lithium\net\http\Response {
if (isset($this->headers['location']) && $this->status['code'] === 200) {
$code = 302;
}
-
- if (!$status = $this->status($code)) {
- throw new Exception('Invalid status code');
- }
-
- $this->_writeHeader($status);
+ $this->_writeHeader($this->status($code) ?: $this->status(500));
foreach ($this->headers as $name => $value) {
$key = strtolower($name);
@@ -102,7 +124,10 @@ class Response extends \lithium\net\http\Response {
$this->_writeHeader("{$name}: {$value}");
}
}
- $chunked = str_split(join("\r\n", (array) $this->body), $this->_config['buffer']);
+ if ($code == 302 || $code == 204) {
+ return;
+ }
+ $chunked = $this->body(null, $this->_config);
foreach ($chunked as $chunk) {
echo $chunk;
diff --git a/libraries/lithium/action/readme.wiki b/libraries/lithium/action/readme.wiki
old mode 100755
new mode 100644
diff --git a/libraries/lithium/analysis/Debugger.php b/libraries/lithium/analysis/Debugger.php
index f021402..891f462 100644
--- a/libraries/lithium/analysis/Debugger.php
+++ b/libraries/lithium/analysis/Debugger.php
@@ -2,16 +2,24 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\analysis;
-use \lithium\util\String;
+use ReflectionClass;
+use lithium\util\String;
+use lithium\analysis\Inspector;
+/**
+ * The `Debugger` class provides basic facilities for generating and rendering meta-data about the
+ * state of an application in its current context.
+ */
class Debugger extends \lithium\core\Object {
+ protected static $_closureCache = array();
+
/**
* Outputs a stack trace based on the supplied options.
*
@@ -34,7 +42,8 @@ class Debugger extends \lithium\core\Object {
'start' => 0,
'scope' => array(),
'trace' => array(),
- 'includeScope' => true
+ 'includeScope' => true,
+ 'closures' => true,
);
$options += $defaults;
@@ -64,6 +73,9 @@ class Debugger extends \lithium\core\Object {
}
}
+ if ($options['closures'] && strpos($function, '{closure}') !== false) {
+ $function = static::_closureDef($backtrace[$i], $function);
+ }
if (in_array($function, array('call_user_func_array', 'trigger_error'))) {
continue;
}
@@ -104,6 +116,7 @@ class Debugger extends \lithium\core\Object {
*/
public static function export($var) {
$export = var_export($var, true);
+
if (is_array($var)) {
$replace = array(" (", " )", " ", " )", "=> \n\t");
$with = array("(", ")", "\t", "\t)", "=> ");
@@ -111,6 +124,76 @@ class Debugger extends \lithium\core\Object {
}
return $export;
}
+
+ /**
+ * Locates original location of closures.
+ *
+ * @param mixed $reference File or class name to inspect.
+ * @param integer $callLine Line number of class reference.
+ */
+ protected static function _definition($reference, $callLine) {
+ if (file_exists($reference)) {
+ foreach (array_reverse(token_get_all(file_get_contents($reference))) as $token) {
+ if (!is_array($token) || $token[2] > $callLine) {
+ continue;
+ }
+ if ($token[0] === T_FUNCTION) {
+ return $token[2];
+ }
+ }
+ return;
+ }
+ list($class, $method) = explode('::', $reference);
+
+ if (!class_exists($class)) {
+ return;
+ }
+
+ $classRef = new ReflectionClass($class);
+ $methodInfo = Inspector::info($reference);
+ $methodDef = join("\n", Inspector::lines($classRef->getFileName(), range(
+ $methodInfo['start'] + 1, $methodInfo['end'] - 1
+ )));
+
+ foreach (array_reverse(token_get_all("<?php {$methodDef} ?>")) as $token) {
+ if (!is_array($token) || $token[2] > $callLine) {
+ continue;
+ }
+ if ($token[0] === T_FUNCTION) {
+ return $token[2] + $methodInfo['start'];
+ }
+ }
+ }
+
+ protected static function _closureDef($frame, $function) {
+ $reference = '::';
+ $frame += array('file' => '??', 'line' => '??');
+ $cacheKey = "{$frame['file']}@{$frame['line']}";
+
+ if (isset(static::$_closureCache[$cacheKey])) {
+ return static::$_closureCache[$cacheKey];
+ }
+
+ if ($class = Inspector::classes(array('file' => $frame['file']))) {
+ foreach (Inspector::methods(key($class), 'extents') as $method => $extents) {
+ $line = $frame['line'];
+
+ if (!($extents[0] <= $line && $line <= $extents[1])) {
+ continue;
+ }
+ $class = key($class);
+ $reference = "{$class}::{$method}";
+ $function = "{$reference}()::{closure}";
+ break;
+ }
+ } else {
+ $reference = $frame['file'];
+ $function = "{$reference}::{closure}";
+ }
+ $line = static::_definition($reference, $frame['line']) ?: '?';
+ $function .= " @ {$line}";
+ return static::$_closureCache[$cacheKey] = $function;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/analysis/Docblock.php b/libraries/lithium/analysis/Docblock.php
index e9f2af4..6b5ee7f 100644
--- a/libraries/lithium/analysis/Docblock.php
+++ b/libraries/lithium/analysis/Docblock.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -12,62 +12,40 @@ namespace lithium\analysis;
* A source code doc block parser.
*
* This parser may be used as the basis for a variety of secondary tools, including
- * a reflection-based API generator, a code metrics analyzer, and various other possible
- * use cases.
+ * a reflection-based API generator, a code metrics analyzer, and various other code or structural
+ * analysis tools.
*/
class Docblock extends \lithium\core\StaticObject {
/**
+ * List of supported docblock tags.
+ *
+ * @var array
+ */
+ public static $tags = array(
+ 'todo', 'discuss', 'fix', 'important', 'var',
+ 'param', 'return', 'throws', 'see', 'link',
+ 'task', 'dependencies', 'filter'
+ );
+
+ /**
* Parses a doc block into its major components of `description`, `text` and `tags`.
*
- * @param string $description The doc block string to be parsed
+ * @param string $comment The doc block string to be parsed
* @return array An associative array of the parsed comment, whose keys are `description`,
- * `text` and `tags`
- * @todo Implement text
+ * `text` and `tags`.
*/
- public static function comment($description) {
+ public static function comment($comment) {
$text = null;
$tags = array();
- $description = trim(preg_replace('/^(\s*\/\*\*|\s*\*\/|\s*\* ?)/m', '', $description));
-
- if (!(preg_match_all('/\n@(\w+)\s+/', $description, $tagNames))) {
- return compact('description', 'text', 'tags');
- }
- $tagContents = preg_split('/\n@\w+\s+/ms', $description);
-
- foreach (array_values(array_slice($tagContents, 1)) as $i => $desc) {
- $description = trim(str_replace("@{$tagNames[1][$i]} {$desc}", '', $description));
- $tag = $tagNames[1][$i];
+ $description = null;
+ $comment = trim(preg_replace('/^(\s*\/\*\*|\s*\*{1,2}\/|\s*\* ?)/m', '', $comment));
+ $comment = str_replace("\r\n", "\n", $comment);
- if (isset($tags[$tag])) {
- $tags[$tag] = (array) $tags[$tag];
- $tags[$tag][] = $desc;
- } else {
- $tags[$tag] = $desc;
- }
- }
-
- if (isset($tags['param'])) {
- $params = $tags['param'];
- $tags['params'] = array();
-
- foreach ((array) $params as $param) {
- $param = explode(' ', $param, 3);
- $type = $name = $text = null;
-
- foreach (array('type', 'name', 'text') as $i => $key) {
- if (!isset($param[$i])) {
- break;
- }
- ${$key} = $param[$i];
- }
- if (!empty($name)) {
- $tags['params'][$name] = compact('type', 'text');
- }
- }
- unset($tags['param']);
+ if ($items = preg_split('/\n@/ms', $comment, 2)) {
+ list($description, $tags) = $items + array('', '');
+ $tags = $tags ? static::tags("@{$tags}") : array();
}
- $text = '';
if (strpos($description, "\n\n")) {
list($description, $text) = explode("\n\n", $description, 2);
@@ -80,31 +58,64 @@ class Docblock extends \lithium\core\StaticObject {
/**
* Parses `@<tagname>` docblock tags and their descriptions from a docblock.
*
- * Currently supported tags are `todo`, `discuss`, `fix` and `important`.
+ * See the `$tags` property for the list of supported tags.
*
- * @param string $str The string to be parsed for tags
- * @param string $options Options array.
- * @return array A numerically indexed array of a associative arrays, with `type`, `text`
- * and `line` keys.
- * @todo Actually implement useful $options
+ * @param string $string The string to be parsed for tags
+ * @return array Returns an array where each docblock tag is a key name, and the corresponding
+ * values are either strings (if one of each tag), or arrays (if multiple of the same
+ * tag).
*/
- public static function parse($str, array $options = array()) {
- $tagTypes = array('todo', 'discuss', 'fix', 'important');
- $tags = '/@(?P<type>' . join('|', $tagTypes) . ')\s(?P<text>.+)$/mi';
+ public static function tags($string) {
+ $regex = '/\n@(?P<type>' . join('|', static::$tags) . ")/msi";
+ $string = trim($string);
- if (!preg_match_all($tags, $str, $matches, PREG_SET_ORDER ^ PREG_OFFSET_CAPTURE)) {
- return false;
+ $result = preg_split($regex, "\n$string", -1, PREG_SPLIT_DELIM_CAPTURE);
+ $tags = array();
+
+ for ($i = 1; $i < count($result) - 1; $i += 2) {
+ $type = trim(strtolower($result[$i]));
+ $text = trim($result[$i + 1]);
+
+ if (isset($tags[$type])) {
+ $tags[$type] = is_array($tags[$type]) ? $tags[$type] : (array) $tags[$type];
+ $tags[$type][] = $text;
+ } else {
+ $tags[$type] = $text;
+ }
+ }
+
+ if (isset($tags['param'])) {
+ $params = $tags['param'];
+ $tags['params'] = static::_params((array) $tags['param']);
+ unset($tags['param']);
}
- $r = array();
+ return $tags;
+ }
- foreach ($matches as $match) {
- list($type, $offset) = $match['type'];
- list($text) = $match['text'];
- $line = preg_match_all('/\r?\n/', substr($str, 0, $offset), $matches) + 1;
- $type = strtolower($type);
- $r[] = compact('type', 'text', 'line');
+ /**
+ * Parses `@param` docblock tags to separate out the parameter type from the description.
+ *
+ * @param array $params An array of `@param` tags, as parsed from the `tags()` method.
+ * @return array Returns an array where each key is a parameter name, and each value is an
+ * associative array containing `'type'` and `'text'` keys.
+ */
+ protected static function _params(array $params) {
+ $result = array();
+ foreach ($params as $param) {
+ $param = explode(' ', $param, 3);
+ $type = $name = $text = null;
+
+ foreach (array('type', 'name', 'text') as $i => $key) {
+ if (!isset($param[$i])) {
+ break;
+ }
+ ${$key} = $param[$i];
+ }
+ if ($name) {
+ $result[$name] = compact('type', 'text');
+ }
}
- return $r;
+ return $result;
}
}
diff --git a/libraries/lithium/analysis/Inspector.php b/libraries/lithium/analysis/Inspector.php
index 6a4a772..2f12c19 100644
--- a/libraries/lithium/analysis/Inspector.php
+++ b/libraries/lithium/analysis/Inspector.php
@@ -2,24 +2,24 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\analysis;
-use \Exception;
-use \ReflectionClass;
-use \ReflectionException;
-use \lithium\core\Libraries;
-use \lithium\util\Collection;
+use Exception;
+use ReflectionClass;
+use ReflectionProperty;
+use ReflectionException;
+use lithium\core\Libraries;
/**
- * General source code inspector
+ * General source code inspector.
*
* This inspector provides a simple interface to the PHP Reflection API that
* can be used to gather information about any PHP source file for purposes of
- * test metrics, static analysis or any number of possible use cases.
+ * test metrics or static analysis.
*/
class Inspector extends \lithium\core\StaticObject {
@@ -143,7 +143,7 @@ class Inspector extends \lithium\core\StaticObject {
$inspector->setAccessible(true);
try {
- $result['value'] = $inspector->getValue($classInspector->newInstance());
+ $result['value'] = $inspector->getValue(static::_class($class));
} catch (Exception $e) {
return null;
}
@@ -163,14 +163,14 @@ class Inspector extends \lithium\core\StaticObject {
*
* @param mixed $class Class name as a string or object instance.
* @param array $options Set of options:
- * -'self': If true (default), only returns lines of methods defined in `$class`,
- * excluding methods from inherited classes.
- * -'methods': An arbitrary list of methods to search, as a string (single method name)
- * or array of method names.
- * -'filter': If true, filters out lines containing only whitespace or braces. Note: for
- * some reason, the Zend engine does not report `switch` and `try` statements as
- * executable lines, as well as parts of multi-line assignment statements, so they are
- * filtered out as well.
+ * - `'self'` _boolean_: If `true` (default), only returns lines of methods defined in
+ * `$class`, excluding methods from inherited classes.
+ * - `'methods'` _array_: An arbitrary list of methods to search, as a string (single
+ * method name) or array of method names.
+ * - `'filter'` _boolean_: If `true`, filters out lines containing only whitespace or
+ * braces. Note: for some reason, the Zend engine does not report `switch` and `try`
+ * statements as executable lines, as well as parts of multi-line assignment
+ * statements, so they are filtered out as well.
* @return array Returns an array of the executable line numbers of the class.
*/
public static function executable($class, array $options = array()) {
@@ -186,15 +186,19 @@ class Inspector extends \lithium\core\StaticObject {
function($str) { return preg_quote($str, '/'); },
$options['blockOpeners']
)));
- $options['pattern'] = "/^(({$pattern})|\\$(.+)\($)/";
+ $pattern = join('|', array(
+ "({$pattern})",
+ "\\$(.+)\($",
+ "\s*['\"]\w+['\"]\s*=>\s*.+[\{\(]$",
+ ));
+ $options['pattern'] = "/^({$pattern})/";
}
if (!$class instanceof ReflectionClass) {
$class = new ReflectionClass(is_object($class) ? get_class($class) : $class);
}
- $result = array_filter(static::methods($class, 'ranges', $options + array(
- 'group' => false
- )));
+ $options += array('group' => false);
+ $result = array_filter(static::methods($class, 'ranges', $options));
if ($options['filter'] && $class->getFileName()) {
$file = explode("\n", "\n" . file_get_contents($class->getFileName()));
@@ -270,6 +274,14 @@ class Inspector extends \lithium\core\StaticObject {
return $result;
}
+ /**
+ * Returns various information on the properties of an object.
+ *
+ * @param mixed $class A string class name or an object instance, from which to get methods.
+ * @param array $options Set of options:
+ * -'self': If true (default), only returns properties defined in `$class`,
+ * excluding properties from inherited classes.
+ */
public static function properties($class, array $options = array()) {
$defaults = array('properties' => array(), 'self' => true);
$options += $defaults;
@@ -283,28 +295,25 @@ class Inspector extends \lithium\core\StaticObject {
}
$options += array('names' => $options['properties']);
- return static::_items($class, 'getProperties', $options)->map(
- function($item) {
- $class = __CLASS__;
- $modifiers = array_values($class::invokeMethod('_modifiers', array($item)));
- $setAccess = (
- array_intersect($modifiers, array('private', 'protected')) != array()
- );
- if ($setAccess) {
- $item->setAccessible(true);
- }
- $result = compact('modifiers') + array(
- 'docComment' => $item->getDocComment(),
- 'name' => $item->getName(),
- 'value' => $item->getValue($item->getDeclaringClass())
- );
- if ($setAccess) {
- $item->setAccessible(false);
- }
- return $result;
- },
- array('collect' => false)
- );
+ return static::_items($class, 'getProperties', $options)->map(function($item) {
+ $class = __CLASS__;
+ $modifiers = array_values($class::invokeMethod('_modifiers', array($item)));
+ $setAccess = (
+ array_intersect($modifiers, array('private', 'protected')) != array()
+ );
+ if ($setAccess) {
+ $item->setAccessible(true);
+ }
+ $result = compact('modifiers') + array(
+ 'docComment' => $item->getDocComment(),
+ 'name' => $item->getName(),
+ 'value' => $item->getValue($item->getDeclaringClass())
+ );
+ if ($setAccess) {
+ $item->setAccessible(false);
+ }
+ return $result;
+ }, array('collect' => false));
}
/**
@@ -323,16 +332,16 @@ class Inspector extends \lithium\core\StaticObject {
* @todo Add an $options parameter with a 'context' flag, to pull in n lines of context.
*/
public static function lines($data, $lines) {
- if (!strpos($data, "\n")) {
+ if (!strpos($data, PHP_EOL)) {
if (!file_exists($data)) {
$data = Libraries::path($data);
if (!file_exists($data)) {
return null;
}
}
- $data = "\n" . file_get_contents($data);
+ $data = PHP_EOL . file_get_contents($data);
}
- $c = explode("\n", $data);
+ $c = explode(PHP_EOL, $data);
if (!count($c) || !count($lines)) {
return null;
@@ -374,18 +383,22 @@ class Inspector extends \lithium\core\StaticObject {
$options += $defaults;
$list = get_declared_classes();
+ $files = get_included_files();
$classes = array();
- if (!empty($options['file'])) {
- $loaded = new Collection(array('items' => array_map(
+ if ($file = $options['file']) {
+ $loaded = static::_instance('collection', array('data' => array_map(
function($class) { return new ReflectionClass($class); }, $list
)));
+ $classFiles = $loaded->getFileName();
- if (!in_array($options['file'], $loaded->getFileName())) {
- include $options['file'];
+ if (in_array($file, $files) && !in_array($file, $classFiles)) {
+ return array();
+ }
+ if (!in_array($file, $classFiles)) {
+ include $file;
$list = array_diff(get_declared_classes(), $list);
} else {
- $file = $options['file'];
$filter = function($class) use ($file) { return $class->getFileName() == $file; };
$list = $loaded->find($filter)->getName();
}
@@ -420,7 +433,9 @@ class Inspector extends \lithium\core\StaticObject {
$join = function ($i) { return join('', $i); };
foreach ((array) $classes as $class) {
- $data = file_get_contents(Libraries::path($class));
+ $data = explode("\n", file_get_contents(Libraries::path($class)));
+ $data = "<?php \n" . join("\n", preg_grep('/^\s*use /', $data)) . "\n ?>";
+
$classes = array_map($join, Parser::find($data, 'use *;', array(
'return' => 'content',
'lineBreaks' => true,
@@ -434,7 +449,7 @@ class Inspector extends \lithium\core\StaticObject {
$classes = static::info($class . '::$_classes', array('value'));
if (isset($classes['value'])) {
- $dynamic = array_merge($dynamic, array_map($trim, $classes['value']));
+ $dynamic = array_merge($dynamic, array_map($trim, array_values($classes['value'])));
}
}
@@ -446,6 +461,23 @@ class Inspector extends \lithium\core\StaticObject {
}
/**
+ * Returns an instance of the given class without directly instantiating it. Inspired by the
+ * work of Sebastian Bergmann on the PHP Object Freezer project.
+ *
+ * @link http://sebastian-bergmann.de/archives/831-Freezing-and-Thawing-PHP-Objects.html
+ * Freezing and Thawing PHP Objects
+ * @param string $class The name of the class to return an instance of.
+ * @return object Returns an instance of the object given by `$class` without calling that
+ * class' constructor.
+ */
+ protected static function _class($class) {
+ if (!class_exists($class)) {
+ throw new RuntimeException(sprintf('Class `%s` could not be found.', $class));
+ }
+ return unserialize(sprintf('O:%d:"%s":0:{}', strlen($class), $class));
+ }
+
+ /**
* Helper method to get an array of `ReflectionMethod` or `ReflectionProperty` objects, wrapped
* in a `Collection` object, and filtered based on a set of options.
*
@@ -460,26 +492,39 @@ class Inspector extends \lithium\core\StaticObject {
protected static function _items($class, $method, $options) {
$defaults = array('names' => array(), 'self' => true, 'public' => true);
$options += $defaults;
- $items = $class->{$method}();
+
+ $params = array(
+ 'getProperties' => ReflectionProperty::IS_PUBLIC | (
+ $options['public'] ? 0 : ReflectionProperty::IS_PROTECTED
+ )
+ );
+ $data = isset($params[$method]) ? $class->{$method}($params[$method]) : $class->{$method}();
if (!empty($options['names'])) {
- $items = array_filter($items, function($item) use ($options) {
+ $data = array_filter($data, function($item) use ($options) {
return in_array($item->getName(), (array) $options['names']);
});
}
if ($options['self']) {
- $items = array_filter($items, function($item) use ($class) {
+ $data = array_filter($data, function($item) use ($class) {
return ($item->getDeclaringClass()->getName() == $class->getName());
});
}
if ($options['public']) {
- $items = array_filter($items, function($item) { return $item->isPublic(); });
+ $data = array_filter($data, function($item) { return $item->isPublic(); });
}
- return new static::$_classes['collection'](compact('items'));
+ return static::_instance('collection', compact('data'));
}
+ /**
+ * Helper method to determine if a class applies to a list of modifiers.
+ *
+ * @param string $inspector ReflectionClass instance.
+ * @param string $list List of modifiers to test.
+ * @return boolean Test result.
+ */
protected static function _modifiers($inspector, $list = array()) {
$list = $list ?: array('public', 'private', 'protected', 'abstract', 'final', 'static');
return array_filter($list, function($modifier) use ($inspector) {
diff --git a/libraries/lithium/analysis/Logger.php b/libraries/lithium/analysis/Logger.php
index eb7ce26..b02d4d1 100644
--- a/libraries/lithium/analysis/Logger.php
+++ b/libraries/lithium/analysis/Logger.php
@@ -2,16 +2,52 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\analysis;
-use \lithium\util\String;
-use \lithium\core\Libraries;
-use \lithium\util\Collection;
+use UnexpectedValueException;
+/**
+ * The `Logger` class provides a consistent, application-wide interface for configuring and writing
+ * log messages. As with other subclasses of `Adaptable`, `Logger` can be configured with a series
+ * of named configurations, each containing a log adapter to write to. `Logger` exposes a single
+ * method, `write()`, which can write to one or more log adapters.
+ *
+ * When configuring adapters, you may specify one or more priorities for each, using the
+ * `'priority'` key. This key can be a single priority level (string), or an array of multiple
+ * levels. When a log message is written, all adpaters that are configured to accept the priority
+ * level with which the message was written will receive the message.
+ *
+ * {{{
+ * Logger::config(array(
+ * 'default' => array('adapter' => 'Syslog'),
+ * 'badnews' => array(
+ * 'adapter' => 'File',
+ * 'priority' => array('emergency', 'alert', 'critical', 'error')
+ * )
+ * ));
+ * }}}
+ *
+ * In the above configuration, all messages will be written to the system log (`syslogd`), but only
+ * messages with the priority `error` or higher will be logged to a file. Messages can then be
+ * written to the log(s) using the `write()` method:
+ *
+ * {{{ Logger::write('alert', 'This is an alert-level message that will be logged in 2 places'); }}}
+ *
+ * Messages can also be written using the log priorty as a method name:
+ *
+ * {{{ Logger::alert('This is an alert-level message that will be logged in 2 places'); }}}
+ *
+ * This works identically to the above. The message priority levels which `Logger` supports are as
+ * follows: `emergency`, `alert`, `critical`, `error`, `warning`, `notice`, `info` and `debug`.
+ * Attempting to use any other priority level will raise an exception. See the list of available
+ * adapters for more information on what adapters are available, and how to configure them.
+ *
+ * @see lithium\analysis\logger\adapter
+ */
class Logger extends \lithium\core\Adaptable {
/**
@@ -19,7 +55,7 @@ class Logger extends \lithium\core\Adaptable {
*
* @var object `Collection` of logger configurations.
*/
- protected static $_configurations = null;
+ protected static $_configurations = array();
/**
* Libraries::locate() compatible path to adapters for this class.
@@ -30,26 +66,117 @@ class Logger extends \lithium\core\Adaptable {
protected static $_adapters = 'adapter.analysis.logger';
/**
- * Writes `$message` to the log specified by the `$type` configuration.
+ * An array of valid message priorities.
*
- * @param string $type Configuration to be used for writing.
- * @param string $message Message to be written.
- * @return boolean `True` on successful write, `false` otherwise.
+ * @var array
*/
- public static function write($type, $message) {
- if (!$config = static::_config($type)) {
- return false;
- }
+ protected static $_priorities = array(
+ 'emergency' => 0,
+ 'alert' => 1,
+ 'critical' => 2,
+ 'error' => 3,
+ 'warning' => 4,
+ 'notice' => 5,
+ 'info' => 6,
+ 'debug' => 7
+ );
+
+ /**
+ * Writes a message to one or more log adapters, where the adapters that are written to are the
+ * ones that respond to the given priority level.
+ *
+ * @param string $priority The priority of the log message to be written.
+ * @param string $message The message to be written.
+ * @param array $options An array of adapter-specific options that may be passed when writing
+ * log messages. Some options are also handled by `Logger` itself:
+ * - `'name'` _string_: This option can be specified if you wish to write to a
+ * specific adapter configuration, instead of writing to the adapter(s) that
+ * respond to the given priority.
+ * @return boolean Returns `true` if all log writes succeeded, or `false` if _any or all_ writes
+ * failed.
+ * @throws UnexpectedValueException If the value of `$priority` is not a defined priority value,
+ * an `UnexpectedValueException` will be thrown.
+ */
+ public static function write($priority, $message, array $options = array()) {
+ $defaults = array('name' => null);
+ $options += $defaults;
+ $result = true;
- $methods = array($type => static::adapter($type)->write($type, $message));
- $result = false;
+ if ($name = $options['name']) {
+ $methods = array($name => static::adapter($name)->write($priority, $message, $options));
+ } elseif (!isset(static::$_priorities[$priority])) {
+ $message = "Attempted to write log message with invalid priority `{$priority}`.";
+ throw new UnexpectedValueException($message);
+ } else {
+ $methods = static::_configsByPriority($priority, $message, $options);
+ }
foreach ($methods as $name => $method) {
- $params = compact('type', 'message');
+ $params = compact('priority', 'message', 'options');
$config = static::_config($name);
- $result = $result || static::_filter(__METHOD__, $params, $method, $config['filters']);
+ $result &= static::_filter(__FUNCTION__, $params, $method, $config['filters']);
+ }
+ return $methods ? $result : false;
+ }
+
+ /**
+ * Acts as a proxy for the `write()` method, allowing log message priority names to be called as
+ * methods, i.e.:
+ * {{{
+ * Logger::emergency('Something bad happened.');
+ * // This is equivalent to Logger::write('emergency', 'Something bad happened')
+ * }}}
+ *
+ * @param string $priority The name of the method called on the `Logger` class. This should map
+ * to a log type.
+ * @param array $params An array of parameters passed in the method.
+ * @return boolean Returns `true` or `false`, depending on the success of the `write()` method.
+ */
+ public static function __callStatic($priority, $params) {
+ $params += array(null, array());
+ return static::write($priority, $params[0], $params[1]);
+ }
+
+ /**
+ * This method is called automatically to initialize the default configuration of a log adapter,
+ * such that the adapter defaults to accepting log messages of any priority (i.e. the
+ * `'priority'` key is set to `true`).
+ *
+ * @param string $name The name of the logger configuration.
+ * @param array $config The logger configuration as specified in application code.
+ * @return array Returns an array of configuration data, merged with default values.
+ */
+ protected static function _initConfig($name, $config) {
+ $defaults = array('priority' => true);
+ return parent::_initConfig($name, $config) + $defaults;
+ }
+
+ /**
+ * Gets the names of the adapter configurations that respond to a specific priority. The list
+ * of adapter configurations returned will be used to write a message with the given priority.
+ *
+ * @param string $priority The priority level of a message to be written.
+ * @param string $message The message to write to the adapter.
+ * @param array $options Adapter-specific options.
+ * @return array Returns an array of names of configurations which are set up to respond to the
+ * message priority specified in `$priority`, or configured to respond to _all_ message
+ * priorities.
+ */
+ protected static function _configsByPriority($priority, $message, array $options = array()) {
+ $configs = array();
+ $key = 'priority';
+
+ foreach (array_keys(static::$_configurations) as $name) {
+ $config = static::config($name);
+ $nameMatch = ($config[$key] === true || $config[$key] === $priority);
+ $arrayMatch = (is_array($config[$key]) && in_array($priority, $config[$key]));
+
+ if ($nameMatch || $arrayMatch) {
+ $method = static::adapter($name)->write($priority, $message, $options);
+ $method ? $configs[$name] = $method : null;
+ }
}
- return $result;
+ return $configs;
}
}
diff --git a/libraries/lithium/analysis/Parser.php b/libraries/lithium/analysis/Parser.php
index 9310993..4969289 100644
--- a/libraries/lithium/analysis/Parser.php
+++ b/libraries/lithium/analysis/Parser.php
@@ -2,18 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\analysis;
-use \ReflectionClass;
-use \lithium\core\Libraries;
-use \lithium\util\Collection;
-use \lithium\util\Validator;
-use \lithium\util\Set;
+use ReflectionClass;
+use lithium\util\Set;
+use lithium\util\Collection;
+/**
+ * The parser class uses PHP's tokenizer to provide methods and tools for performing static analysis
+ * on PHP code.
+ */
class Parser extends \lithium\core\StaticObject {
/**
@@ -35,6 +37,18 @@ class Parser extends \lithium\core\StaticObject {
return $token[($options['id']) ? 'id' : 'name'];
}
+ /**
+ * Splits the provided `$code` into PHP language tokens.
+ *
+ * @param string $code Source code to be tokenized.
+ * @param array $options Options consists of:
+ * -'wrap': Boolean indicating whether or not to wrap the supplied
+ * code in PHP tags.
+ * -'ignore': An array containing PHP language tokens to ignore.
+ * -'include': If supplied, an array of the only language tokens
+ * to include in the output.
+ * @return array An array of tokens in the supplied source code.
+ */
public static function tokenize($code, array $options = array()) {
$defaults = array('wrap' => true, 'ignore' => array(), 'include' => array());
$options += $defaults;
@@ -94,8 +108,8 @@ class Parser extends \lithium\core\StaticObject {
$patternMatch = array();
$ret = $options['return'];
- $tokens = new Collection(array('items' => static::tokenize($code, $options)));
- $pattern = new Collection(array('items' => static::tokenize($pattern, $options)));
+ $tokens = new Collection(array('data' => static::tokenize($code, $options)));
+ $pattern = new Collection(array('data' => static::tokenize($pattern, $options)));
$breaks = function($token) use (&$tokens, &$matches, &$patternMatch, $options) {
if (!$options['lineBreaks']) {
@@ -194,6 +208,16 @@ class Parser extends \lithium\core\StaticObject {
return $results;
}
+ /**
+ * Token pattern matching.
+ *
+ * @param string $code Source code to be analyzed.
+ * @param string $parameters An array containing token patterns to be matched.
+ * @param array $options The list of options to be used when matching `$code`:
+ * - 'ignore': An array of language tokens to ignore.
+ * - 'return': If set to 'content' returns an array of matching tokens.
+ * @return array Array of matching tokens.
+ */
public static function match($code, $parameters, array $options = array()) {
$defaults = array('ignore' => array('T_WHITESPACE'), 'return' => true);
$options += $defaults;
@@ -226,6 +250,13 @@ class Parser extends \lithium\core\StaticObject {
return $results;
}
+ /**
+ * Compares two PHP language tokens.
+ *
+ * @param array $pattern Pattern token.
+ * @param array $token Token to be compared.
+ * @return boolean Match result.
+ */
public static function matchToken($pattern, $token) {
if ($pattern['name'] != $token['name']) {
return false;
@@ -250,6 +281,13 @@ class Parser extends \lithium\core\StaticObject {
return false;
}
+ /**
+ * Helper function to normalize parameters for token matching.
+ *
+ * @see lithium\analysis\Parser::match()
+ * @param array $parameters Params to be normalized.
+ * @return array Normalized parameters.
+ */
protected static function _prepareMatchParams($parameters) {
foreach (Set::normalize($parameters) as $token => $scope) {
if (strpos($token, 'T_') !== 0) {
diff --git a/libraries/lithium/analysis/logger/adapter/Cache.php b/libraries/lithium/analysis/logger/adapter/Cache.php
index 6b1f837..9ea8c9c 100644
--- a/libraries/lithium/analysis/logger/adapter/Cache.php
+++ b/libraries/lithium/analysis/logger/adapter/Cache.php
@@ -2,16 +2,43 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\analysis\logger\adapter;
-use \lithium\util\String;
+use lithium\util\String;
+/**
+ * The `Cache` logger allows log messages to be written to cache configurations set up in
+ * `lithium\storage\Cache`. In order to use this adapter, you must first configure a cache adapter
+ * for it to write to, as follows:
+ *
+ * {{{ lithium\storage\Cache::config(array(
+ * 'storage' => array('adapter' => 'Redis', 'host' => '127.0.0.1:6379')
+ * ));}}}
+ *
+ * Then, you can configure the `Cache` logger with the `'storage'` config:
+ * {{{ lithium\analysis\Logger::config(array(
+ * 'debug' => array('adapter' => 'Cache', 'config' => 'storage')
+ * ));
+ * }}}
+ *
+ * You can then send messages to the logger which will be written to the cache store:
+ * {{{
+ * lithium\analysis\Logger::write('debug', 'This message will be written to a Redis data store.');
+ * }}}
+ *
+ * @see lithium\storage\Cache
+ */
class Cache extends \lithium\core\Object {
+ /**
+ * Classes used by `Cache`.
+ *
+ * @var array
+ */
protected $_classes = array(
'cache' => '\lithium\storage\Cache'
);
@@ -41,7 +68,7 @@ class Cache extends \lithium\core\Object {
public function write($type, $message) {
$config = $this->_config;
- return function($self, $params, $chain) use ($config) {
+ return function($self, $params) use ($config) {
$params += array('timestamp' => strtotime('now'));
$key = $config['key'];
$key = is_callable($key) ? $key($params) : String::insert($key, $params);
diff --git a/libraries/lithium/analysis/logger/adapter/File.php b/libraries/lithium/analysis/logger/adapter/File.php
index cd1f4b8..ca9ecd7 100644
--- a/libraries/lithium/analysis/logger/adapter/File.php
+++ b/libraries/lithium/analysis/logger/adapter/File.php
@@ -2,41 +2,81 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\analysis\logger\adapter;
-use \SplFileInfo;
-use \DirectoryIterator;
+use lithium\util\String;
+use lithium\core\Libraries;
+/**
+ * A simple log adapter that writes messages to files. By default, messages are written to
+ * `app/resources/tmp/logs/<type>.log`, where `<type>` is the log message priority level.
+ *
+ * {{{
+ * use lithium\analysis\Logger;
+ *
+ * Logger::config(array(
+ * 'simple' => array('adapter' => 'File')
+ * ));
+ * Logger::write('debug', 'Something happened!');
+ * }}}
+ *
+ * This will cause the message and the timestamp of the log event to be written to
+ * `app/resources/tmp/logs/debug.log`. For available configuration options for this adapter, see
+ * the `__construct()` method.
+ *
+ * @see lithium\analysis\logger\adapter\File::__construct()
+ */
class File extends \lithium\core\Object {
/**
* Class constructor.
*
- * @param array $config
+ * @see lithium\util\String::insert()
+ * @param array $config Settings used to configure the adapter. Available options:
+ * - `'path'` _string_: The directory to write log files to. Defaults to
+ * `<app>/resources/tmp/logs`.
+ * - `'timestamp'` _string_: The `date()`-compatible timestamp format. Defaults to
+ * `'Y-m-d H:i:s'`.
+ * - `'file'` _closure_: A closure which accepts two parameters: an array
+ * containing the current log message details, and an array containing the `File`
+ * adapter's current configuration. It must then return a file name to write the
+ * log message to. The default will produce a log file name corresponding to the
+ * priority of the log message, i.e. `"debug.log"` or `"alert.log"`.
+ * - `'format'` _string_: A `String::insert()`-compatible string that specifies how
+ * the log message should be formatted. The default format is
+ * `"{:timestamp} {:message}\n"`.
* @return void
*/
public function __construct(array $config = array()) {
- $defaults = array('path' => LITHIUM_APP_PATH . '/resources/tmp/logs');
+ $defaults = array(
+ 'path' => Libraries::get(true, 'resources') . '/tmp/logs',
+ 'timestamp' => 'Y-m-d H:i:s',
+ 'file' => function($data, $config) { return "{$data['priority']}.log"; },
+ 'format' => "{:timestamp} {:message}\n",
+ );
parent::__construct($config + $defaults);
}
/**
- * Appends $data to file $type.
+ * Appends a message to a log file.
*
- * @param string $type
- * @param string $message
- * @return boolean `True` on successful write, `false` otherwise.
+ * @see lithium\analysis\Logger::$_priorities
+ * @param string $priority The message priority. See `Logger::$_priorities`.
+ * @param string $message The message to write to the log.
+ * @return closure Returns a closure that writes to the log, which can be wrapped in filters.
*/
- public function write($type, $message) {
- $path = $this->_config['path'];
+ public function write($priority, $message) {
+ $config = $this->_config;
- return function($self, $params, $chain) use (&$path) {
- extract($params);
- return file_put_contents("$path/$type.log", "{$message}\n", FILE_APPEND);
+ return function($self, $params) use (&$config) {
+ $path = $config['path'] . '/' . $config['file']($params, $config);
+ $params['timestamp'] = date($config['timestamp']);
+ $message = String::insert($config['format'], $params);
+ return file_put_contents($path, $message, FILE_APPEND);
};
}
}
diff --git a/libraries/lithium/analysis/logger/adapter/Growl.php b/libraries/lithium/analysis/logger/adapter/Growl.php
new file mode 100644
index 0000000..0df88fb
--- /dev/null
+++ b/libraries/lithium/analysis/logger/adapter/Growl.php
@@ -0,0 +1,205 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\analysis\logger\adapter;
+
+use lithium\util\Inflector;
+use lithium\core\NetworkException;
+
+/**
+ * The `Growl` logger implements support for the [ Growl](http://growl.info/) notification system
+ * for Mac OS X. Writing to this logger will display small, customizable status messages on the
+ * screen.
+ */
+class Growl extends \lithium\core\Object {
+
+ /**
+ * The Growl protocol version used to send messages.
+ */
+ const PROTOCOL_VERSION = 1;
+
+ /**
+ * There are two types of messages sent to Growl: one to register applications, and one to send
+ * notifications. This type registers the application with Growl's settings.
+ */
+ const TYPE_REG = 0;
+
+ /**
+ * This message type is for sending notifications to Growl.
+ */
+ const TYPE_NOTIFY = 1;
+
+ /**
+ * Holds the socket connection resource used to send messages to Growl.
+ *
+ * @var resource
+ */
+ public $connection = null;
+
+ /**
+ * Flag indicating whether the logger has successfully registered with the Growl server.
+ * Registration only needs to happen once, but may fail for several reasons, including inability
+ * to connect to the server, or the server requires a password which has not been specified.
+ *
+ * @var boolean
+ */
+ protected $_registered = false;
+
+ /**
+ * Growl logger constructor. Accepts an array of settings which are merged with the default
+ * settings and used to create the connection and handle notifications.
+ *
+ * @see lithium\analysis\Logger::write()
+ * @param array $config The settings to configure the logger. Available settings are as follows:
+ * - `'name`' _string_: The name of the application as it should appear in Growl's
+ * system settings. Defaults to the directory name containing your application.
+ * - `'host'` _string_: The Growl host with which to communicate, usually your
+ * local machine. Use this setting to send notifications to another machine on
+ * the network. Defaults to `'127.0.0.1'`.
+ * - `'port'` _integer_: Port of the host machine. Defaults to the standard Growl
+ * port, `9887`.
+ * - `'password'` _string_: Only required if the host machine requires a password.
+ * If notification or registration fails, check this against the host machine's
+ * Growl settings.
+ * - '`protocol'` _string_: Protocol to use when opening socket communication to
+ * Growl. Defaults to `'udp'`.
+ * - `'title'` _string_: The default title to display when showing Growl messages.
+ * The default value is the same as `'name'`, but can be changed on a per-message
+ * basis by specifying a `'title'` key in the `$options` parameter of
+ * `Logger::write()`.
+ * - `'notification'` _array_: A list of message types you wish to register with
+ * Growl to be able to send. Defaults to `array('Errors', 'Messages')`.
+ * @return void
+ */
+ public function __construct(array $config = array()) {
+ $name = basename(LITHIUM_APP_PATH);
+ $defaults = array(
+ 'name' => $name,
+ 'host' => '127.0.0.1',
+ 'port' => 9887,
+ 'password' => null,
+ 'protocol' => 'udp',
+ 'title' => Inflector::humanize($name),
+ 'notifications' => array('Errors', 'Messages'),
+ 'connection' => function($host, $port) {
+ if ($conn = fsockopen($host, $port, $message, $code)) {
+ return $conn;
+ }
+ throw new NetworkException("Growl connection failed: (`{$code}`) `{$message}`.");
+ }
+ );
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Writes `$message` to a new Growl notification.
+ *
+ * @param string $type Not used (all notifications are of the same type).
+ * @param string $message Message to be shown.
+ * @param array $options Any options that are passed to the `notify()` method. See the
+ * `$options` parameter of `notify()`.
+ * @return boolean `True` on successful write, `false` otherwise.
+ */
+ public function write($type, $message, array $options = array()) {
+ if (!$this->_register()) {
+ return;
+ }
+ $_self =& $this;
+
+ return function($self, $params) use (&$_self) {
+ return $_self->notify($params['message'], $params['options']);
+ };
+ }
+
+ /**
+ * Posts a new notification to the Growl server.
+ *
+ * @param string $description Message to be displayed.
+ * @param array $options Options consists of:
+ * -'title': The title of the displayed notification. Displays the
+ * name of the application's parent folder by default.
+ * @return boolean `True` on successful write, `false` otherwise.
+ */
+ public function notify($description = '', $options = array()) {
+ if (!$this->_register()) {
+ return false;
+ }
+ $defaults = array('sticky' => false, 'priority' => 0, 'type' => 'Messages');
+ $options += $defaults + array('title' => $this->_config['title']);
+ $type = $options['type'];
+ $title = $options['title'];
+
+ $message = compact('type', 'title', 'description') + array('app' => $this->_config['name']);
+ $message = array_map('utf8_encode', $message);
+
+ $flags = ($options['priority'] & 7) * 2;
+ $flags = ($options['priority'] < 0) ? $flags |= 8 : $flags;
+ $flags = ($options['sticky']) ? $flags | 256 : $flags;
+
+ $params = array('c2n5', static::PROTOCOL_VERSION, static::TYPE_NOTIFY, $flags);
+ $lengths = array_map('strlen', $message);
+
+ $data = call_user_func_array('pack', array_merge($params, $lengths));
+ $data .= join('', $message);
+ $data .= pack('H32', md5($data . $this->_config['password']));
+
+ if (fwrite($this->connection, $data, strlen($data)) === false) {
+ throw new NetworkException('Could not send notification to Growl Server.');
+ }
+ return true;
+ }
+
+ /**
+ * Growl server connection registration and initialization.
+ *
+ * @return boolean `True` on successful write, `false` otherwise.
+ */
+ protected function _register() {
+ if ($this->_registered) {
+ return true;
+ }
+
+ if (!$this->connection) {
+ $connection = $this->_config['connection'];
+ $this->connection = $connection(
+ "{$this->_config['protocol']}://{$this->_config['host']}", $this->_config['port']
+ );
+ }
+ $app = utf8_encode($this->_config['name']);
+ $nameEnc = $defaultEnc = '';
+
+ foreach ($this->_config['notifications'] as $i => $name) {
+ $name = utf8_encode($name);
+ $nameEnc .= pack('n', strlen($name)) . $name;
+ $defaultEnc .= pack('c', $i);
+ }
+ $data = pack('c2nc2', static::PROTOCOL_VERSION, static::TYPE_REG, strlen($app), $i, $i);
+ $data .= $app . $nameEnc . $defaultEnc;
+ $checksum = pack('H32', md5($data . $this->_config['password']));
+ $data .= $checksum;
+
+ if (fwrite($this->connection, $data, strlen($data)) === false) {
+ throw new NetworkException('Could not send registration to Growl Server.');
+ }
+ return $this->_registered = true;
+ }
+
+ /**
+ * Destructor method. Closes and releases the socket connection to Growl.
+ *
+ * @return void
+ */
+ public function __destruct() {
+ if (is_resource($this->connection)) {
+ fclose($this->connection);
+ unset($this->connection);
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/analysis/logger/adapter/Syslog.php b/libraries/lithium/analysis/logger/adapter/Syslog.php
index 530a7a4..870f106 100644
--- a/libraries/lithium/analysis/logger/adapter/Syslog.php
+++ b/libraries/lithium/analysis/logger/adapter/Syslog.php
@@ -2,58 +2,83 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\analysis\logger\adapter;
/**
- * The Syslog adapter facilitates logging messages to a syslogd backend.
+ * The Syslog adapter facilitates logging messages to a `syslogd` backend. See the constructor for
+ * information on configuring this adapter.
+ *
+ * @see lithium\analysis\logger\adapter\Syslog::__construct()
*/
class Syslog extends \lithium\core\Object {
/**
- * The last connection to have opened syslog. This will determine whether or
- * not the log needs to be closed and reopened.
+ * Flag indicating whether or not the connection to `syslogd` has been opened yet.
*
- * @var string
+ * @var boolean
*/
- protected static $_lastOpenedBy;
+ protected $_isConnected = false;
/**
- * Class constructor
+ * Array that maps `Logger` message priority names to `syslog`-compatible priority constants.
*
- * @param array $config
+ * @var array
+ */
+ protected $_priorities = array(
+ 'emergency' => LOG_EMERG,
+ 'alert' => LOG_ALERT,
+ 'critical' => LOG_CRIT,
+ 'error' => LOG_ERR,
+ 'warning' => LOG_WARNING,
+ 'notice' => LOG_NOTICE,
+ 'info' => LOG_INFO,
+ 'debug' => LOG_DEBUG
+ );
+
+ /**
+ * Class constructor. Configures the `Syslog` adapter instance with the default settings. For
+ * more information on these settings, see the documentation for
+ * [the `openlog()` function](http://php.net/openlog).
+ *
+ * @param array $config Available configuration settings for this adapter:
+ * - `'identity'` _string_: The identity string to be attached to each message in
+ * the system log. This is usually a string that meaningfully identifies your
+ * application. Defaults to `false`.
+ * - `'options'` _integer_: The flags to use when opening the log. Defaults to
+ * `LOG_ODELAY`.
+ * - `'facility'` _integer_: A flag specifying the program to use to log the
+ * messages. See the `openlog()` documentation for more information. Defaults to
+ * `LOG_USER`.
* @return void
*/
public function __construct(array $config = array()) {
- $defaults = array(
- 'identity' => false,
- 'options' => LOG_ODELAY,
- 'facility' => LOG_USER,
- 'priority' => LOG_INFO
- );
+ $defaults = array('identity' => false, 'options' => LOG_ODELAY, 'facility' => LOG_USER);
parent::__construct($config + $defaults);
}
/**
- * Appends `$data` to file `$type`.
+ * Appends `$message` to the system log.
*
- * @param string $type
- * @param string $message
- * @return boolean `True` on successful write, `false` otherwise.
+ * @param string $priority The message priority string. Maps to a `syslogd` priority constant.
+ * @param string $message The message to write.
+ * @return boolean Returns `true` on successful write, `false` otherwise.
*/
- public function write($type, $message) {
- if (static::$_lastOpenedBy != $type) {
- closelog(); // Close previously opened log (doesn't matter if none opened)
- openlog($this->_config['identity'], $this->_config['options'], $this->_config['facility']);
- static::$_lastOpenedBy = $type;
- }
+ public function write($priority, $message) {
+ $config = $this->_config;
+ $_priorities = $this->_priorities;
- $priority = $this->_config['priority'];
+ if (!$this->_isConnected) {
+ closelog();
+ openlog($config['identity'], $config['options'], $config['facility']);
+ $this->_isConnected = true;
+ }
- return function($self, $params, $chain) use ($priority) {
+ return function($self, $params) use ($_priorities) {
+ $priority = $_priorities[$params['priority']];
return syslog($priority, $params['message']);
};
}
diff --git a/libraries/lithium/console/Command.php b/libraries/lithium/console/Command.php
index 89c749d..998c438 100644
--- a/libraries/lithium/console/Command.php
+++ b/libraries/lithium/console/Command.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console;
-use \Exception;
-use \lithium\console\command\Help;
+use Exception;
+use lithium\console\command\Help;
/**
* The base class to inherit when writing console scripts in Lithium.
@@ -27,16 +27,16 @@ class Command extends \lithium\core\Object {
/**
* A Request object.
*
- * @var object
* @see lithium\console\Request
+ * @var object
*/
public $request;
/**
* A Response object.
*
- * @var object
* @see lithium\console\Response
+ * @var object
*/
public $response;
@@ -46,7 +46,7 @@ class Command extends \lithium\core\Object {
* @var array
*/
protected $_classes = array(
- 'response' => '\lithium\console\Response'
+ 'response' => 'lithium\console\Response'
);
/**
@@ -81,7 +81,7 @@ class Command extends \lithium\core\Object {
parent::_init();
$this->request = $this->_config['request'];
- $this->response = new $this->_classes['response']($this->_config['response']);
+ $this->response = $this->_instance('response', $this->_config['response']);
if (!empty($this->request->params)) {
$params = (array) array_diff_key(
@@ -96,12 +96,12 @@ class Command extends \lithium\core\Object {
/**
* Called by the Dispatcher class to invoke an action.
*
+ * @see lithium\console\Dispatcher
+ * @see lithium\console\Response
* @param string $action name of the method to run
* @param array $args the args from the request
* @param array $options
* @return object The response object associated with this command.
- * @see lithium\console\Dispatcher
- * @see lithium\console\Response
* @todo Implement proper exception catching/throwing.
* @todo Implement filters.
*/
@@ -109,12 +109,14 @@ class Command extends \lithium\core\Object {
try {
$this->response->status = 1;
$result = $this->invokeMethod($action, $args);
+
if (is_int($result)) {
$this->response->status = $result;
} elseif ($result || $result === null) {
$this->response->status = 0;
}
} catch (Exception $e) {
+ $this->error($e->getMessage());
$this->response->status = 1;
}
return $this->response;
@@ -133,6 +135,7 @@ class Command extends \lithium\core\Object {
* @return integer|void
*/
public function out($output = null, $options = array('nl' => 1)) {
+ $options = is_int($options) ? array('nl' => $options) : $options;
return $this->_response('output', $output, $options);
}
@@ -174,8 +177,8 @@ class Command extends \lithium\core\Object {
} else {
$this->out("{$prompt} {$choices} \n [{$options['default']}] > ", false);
}
-
$result = null;
+
do {
$result = trim($this->request->input());
} while (
@@ -236,7 +239,7 @@ class Command extends \lithium\core\Object {
* @return integer
*/
public function nl($number = 1) {
- return str_pad("\n", $number, "\n");
+ return str_repeat("\n", $number);
}
/**
diff --git a/libraries/lithium/console/Dispatcher.php b/libraries/lithium/console/Dispatcher.php
index be3a939..faa719a 100644
--- a/libraries/lithium/console/Dispatcher.php
+++ b/libraries/lithium/console/Dispatcher.php
@@ -2,17 +2,22 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console;
-use \UnexpectedValueException;
-use \lithium\core\Libraries;
-use \lithium\util\String;
-use \lithium\util\Inflector;
+use lithium\core\Libraries;
+use UnexpectedValueException;
+/**
+ * The console dispatcher is responsible for accepting requests from scripts called from the command
+ * line, and executing the appropriate `Command` class(es). The `run()` method accepts an instance
+ * of `lithium\console\Request`, which encapsulates the console environment and any command-line
+ * parameters passed to the script. `Dispatcher` then invokes `lithium\console\Router` to determine
+ * the correct `Command` class to invoke, and which method should be called.
+ */
class Dispatcher extends \lithium\core\StaticObject {
/**
@@ -23,8 +28,8 @@ class Dispatcher extends \lithium\core\StaticObject {
* @var array
*/
protected static $_classes = array(
- 'request' => '\lithium\console\Request',
- 'router' => '\lithium\console\Router'
+ 'request' => 'lithium\console\Request',
+ 'router' => 'lithium\console\Router'
);
/**
@@ -35,16 +40,12 @@ class Dispatcher extends \lithium\core\StaticObject {
* elements array elements of the flag which are present in the route will be modified using a
* `lithium\util\String::insert()`-formatted string.
*
- * For example, to implement action prefixes (i.e. `admin_index()`), set a rule named 'admin',
- * with a value array containing a modifier key for the `action` element of a route, i.e.:
- * `array('action' => 'admin_{:action}')`. See `lithium\console\Dispatcher::config()` for
- * examples on setting rules.
- *
* @see lithium\console\Dispatcher::config()
* @see lithium\util\String::insert()
*/
protected static $_rules = array(
- //'plugin' => array('command' => '{:plugin}.{:command}')
+ 'command' => array(array('lithium\util\Inflector', 'camelize')),
+ 'action' => array(array('lithium\util\Inflector', 'camelize', array(false)))
);
/**
@@ -55,7 +56,7 @@ class Dispatcher extends \lithium\core\StaticObject {
* current configuration, otherwise returns null.
*/
public static function config($config = array()) {
- if (empty($config)) {
+ if (!$config) {
return array('rules' => static::$_rules);
}
foreach ($config as $key => $val) {
@@ -70,11 +71,10 @@ class Dispatcher extends \lithium\core\StaticObject {
* If `$request` is `null`, a new request object is instantiated based on the value of the
* `'request'` key in the `$_classes` array.
*
- * @param object $request An instance of a request object with HTTP request information. If
+ * @param object $request An instance of a request object with console request information. If
* `null`, an instance will be created.
* @param array $options
* @return object The command action result which is an instance of `lithium\console\Response`.
- * @todo Add exception-handling/error page rendering
*/
public static function run($request = null, $options = array()) {
$defaults = array('request' => array());
@@ -83,13 +83,15 @@ class Dispatcher extends \lithium\core\StaticObject {
$params = compact('request', 'options');
$method = __FUNCTION__;
- return static::_filter($method, $params, function($self, $params, $chain) use ($classes) {
- extract($params);
+ return static::_filter($method, $params, function($self, $params) use ($classes) {
+ $request = $params['request'];
+ $options = $params['options'];
$router = $classes['router'];
$request = $request ?: new $classes['request']($options['request']);
$request->params = $router::parse($request);
- $params = $self::invokeMethod('_applyRules', array($request->params));
+
+ $params = $self::applyRules($request->params);
try {
$callable = $self::invokeMethod('_callable', array($request, $params, $options));
@@ -101,36 +103,61 @@ class Dispatcher extends \lithium\core\StaticObject {
}
/**
- * Determines Command to use for current request. If
+ * Determines Command to use for current request.
*
* @param string $request
* @param string $params
* @param string $options
- * @return class \lithium\console\COmmand
+ * @return class lithium\console\Command
*/
protected static function _callable($request, $params, $options) {
$params = compact('request', 'params', 'options');
- return static::_filter(__FUNCTION__, $params, function($self, $params, $chain) {
- extract($params, EXTR_OVERWRITE);
- $name = $class = $params['command'];
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $request = $params['request'];
+ $params = $params['params'];
+ $name = $params['command'];
if (!$name) {
$request->params['args'][0] = $name;
- $name = $class = '\lithium\console\command\Help';
- }
- if ($class[0] !== '\\') {
- $name = Inflector::camelize($class);
- $class = Libraries::locate('command', $name);
+ $name = 'lithium\console\command\Help';
}
-
- if (class_exists($class)) {
+ if (class_exists($class = Libraries::locate('command', $name))) {
return new $class(compact('request'));
}
- throw new UnexpectedValueException("Command `{$name}` not found");
+ throw new UnexpectedValueException("Command `{$name}` not found.");
});
}
/**
+ * Attempts to apply a set of formatting rules from `$_rules` to a `$params` array, where each
+ * formatting rule is applied if the key of the rule in `$_rules` is present and not empty in
+ * `$params`. Also performs sanity checking against `$params` to ensure that no value
+ * matching a rule is present unless the rule check passes.
+ *
+ * @param array $params An array of route parameters to which rules will be applied.
+ * @return array Returns the `$params` array with formatting rules applied to array values.
+ */
+ public static function applyRules($params) {
+ $result = array();
+
+ if (!$params) {
+ return false;
+ }
+
+ foreach (static::$_rules as $name => $rules) {
+ foreach ($rules as $rule) {
+ if (!empty($params[$name]) && isset($rule[0])) {
+ $options = array_merge(
+ array($params[$name]), isset($rule[2]) ? (array) $rule[2] : array()
+ );
+ $result[$name] = call_user_func_array(array($rule[0], $rule[1]), $options);
+ }
+ }
+ }
+ return $result + array_diff_key($params, $result);
+ }
+
+ /**
* Call class method
*
* @param string $callable
@@ -140,54 +167,27 @@ class Dispatcher extends \lithium\core\StaticObject {
*/
protected static function _call($callable, $request, $params) {
$params = compact('callable', 'request', 'params');
- return static::_filter(__FUNCTION__, $params, function($self, $params, $chain) {
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
if (is_callable($callable = $params['callable'])) {
$request = $params['request'];
+ $params = $params['params'];
- if (!method_exists($callable, $request->params['action'])) {
- array_unshift($request->params['args'], $request->params['action']);
- $request->params['action'] = 'run';
+ if (!method_exists($callable, $params['action'])) {
+ array_unshift($params['args'], $request->params['action']);
+ $params['action'] = 'run';
}
$isHelp = (
- !empty($request->params['help']) || !empty($request->params['h'])
- || !method_exists($callable, $request->params['action'])
+ !empty($params['help']) || !empty($params['h'])
+ || !method_exists($callable, $params['action'])
);
if ($isHelp) {
-
- $request->params['action'] = '_help';
+ $params['action'] = '_help';
}
- return $callable($request->params['action'], $request->params['args']);
+ return $callable($params['action'], $params['args']);
}
- throw new UnexpectedValueException("{$callable} not callable");
+ throw new UnexpectedValueException("Callable `{$callable}` is actually not callable.");
});
}
-
- /**
- * Attempts to apply a set of formatting rules from `$_rules` to a `$params` array, where each
- * formatting rule is applied if the key of the rule in `$_rules` is present and not empty in
- * `$params`. Also performs sanity checking against `$params` to ensure that no value
- * matching a rule is present unless the rule check passes.
- *
- * @param array $params An array of route parameters to which rules will be applied.
- * @return array Returns the $params array with formatting rules applied to array values.
- */
- protected static function _applyRules($params) {
- foreach (static::$_rules as $rule => $value) {
- foreach ($value as $k => $v) {
- if (!empty($params[$rule])) {
- $params[$k] = String::insert($v, $params);
- }
-
- $match = preg_replace('/\{:\w+\}/', '@', $v);
- $match = preg_replace('/@/', '.+', preg_quote($match, '/'));
-
- if (preg_match('/' . $match . '/i', $params[$k])) {
- return false;
- }
- }
- }
- return $params;
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/console/Request.php b/libraries/lithium/console/Request.php
index ab63622..7c3acbf 100644
--- a/libraries/lithium/console/Request.php
+++ b/libraries/lithium/console/Request.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -17,17 +17,17 @@ namespace lithium\console;
class Request extends \lithium\core\Object {
/**
- * Arguments for the request.
+ * The raw data passed from the command line
*
* @var array
*/
- public $args = array();
+ public $argv = array();
/**
* Parameters parsed from arguments.
*
- * @var array
* @see lithium\console\Router
+ * @var array
*/
public $params = array(
'command' => null, 'action' => 'run', 'args' => array()
@@ -44,7 +44,7 @@ class Request extends \lithium\core\Object {
* Enviroment variables.
*
* @var array
- **/
+ */
protected $_env = array();
/**
@@ -75,7 +75,7 @@ class Request extends \lithium\core\Object {
$this->_env['working'] = getcwd() ?: null;
$argv = (array) $this->env('argv');
$this->_env['script'] = array_shift($argv);
- $this->args += $argv + (array) $this->_config['args'];
+ $this->argv += $argv + (array) $this->_config['args'];
$this->input = $this->_config['input'];
if (!is_resource($this->_config['input'])) {
@@ -83,14 +83,14 @@ class Request extends \lithium\core\Object {
}
parent::_init();
}
-
+
/**
* Allows request parameters to be accessed as object properties, i.e. `$this->request->action`
* instead of `$this->request->params['action']`.
*
+ * @see lithium\action\Request::$params
* @param string $name The property name/parameter key to return.
* @return mixed Returns the value of `$params[$name]` if it is set, otherwise returns null.
- * @see lithium\action\Request::$params
*/
public function __get($name) {
if (isset($this->params[$name])) {
@@ -98,6 +98,23 @@ class Request extends \lithium\core\Object {
}
}
+ public function __isSet($name) {
+ return isset($this->params[$name]);
+ }
+
+ /**
+ * Get the value of a command line argument at a given key
+ *
+ * @param integer $key
+ * @return mixed returns null if key does not exist or the value of the key in the args array
+ */
+ public function args($key = 0) {
+ if (!empty($this->args[$key])) {
+ return $this->args[$key];
+ }
+ return null;
+ }
+
/**
* Get environment variables.
*
@@ -125,7 +142,9 @@ class Request extends \lithium\core\Object {
$this->shift(--$i);
}
$this->params['command'] = $this->params['action'];
- $this->params['action'] = array_shift($this->params['args']);
+ if (isset($this->params['args'][0])) {
+ $this->params['action'] = array_shift($this->params['args']);
+ }
return $this;
}
@@ -145,7 +164,9 @@ class Request extends \lithium\core\Object {
* @return void
*/
public function __destruct() {
- fclose($this->input);
+ if ($this->input) {
+ fclose($this->input);
+ }
}
}
diff --git a/libraries/lithium/console/Response.php b/libraries/lithium/console/Response.php
index 94b9321..e09979c 100644
--- a/libraries/lithium/console/Response.php
+++ b/libraries/lithium/console/Response.php
@@ -2,33 +2,33 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console;
-use \lithium\util\String;
+use lithium\util\String;
/**
- * Holds current request from console
- *
- *
- **/
+ * The `Response` class is used by other console classes to generate output. It contains stream
+ * resources for writing output and errors, as well as shell coloring information, and the response
+ * status code for the currently-executing command.
+ */
class Response extends \lithium\core\Object {
/**
* Output stream, STDOUT
*
* @var stream
- **/
+ */
public $output = null;
/**
* Error stream, STDERR
*
* @var stream
- **/
+ */
public $error = null;
/**
@@ -94,10 +94,14 @@ class Response extends \lithium\core\Object {
*
* @return void
*
- **/
+ */
public function __destruct() {
- fclose($this->output);
- fclose($this->error);
+ if ($this->output) {
+ fclose($this->output);
+ }
+ if ($this->error) {
+ fclose($this->error);
+ }
}
/**
diff --git a/libraries/lithium/console/Router.php b/libraries/lithium/console/Router.php
index 743936e..e296927 100644
--- a/libraries/lithium/console/Router.php
+++ b/libraries/lithium/console/Router.php
@@ -2,26 +2,26 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console;
/**
- * Router parses incoming request
- *
- *
- **/
+ * The `Router` class uses an instance of `lithium\console\Request`, which represents an incoming
+ * command-line invokation, to parse the correct command, and sub-command(s) and parameters, which
+ * are used by `lithium\console\Dispatcher` to load and execute the proper `Command` class.
+ */
class Router extends \lithium\core\Object {
/**
* Parse incoming request from console
*
- * @param object $request \lithium\console\Request
+ * @param object $request lithium\console\Request
* @return array $params
*
- **/
+ */
public static function parse($request = null) {
$params = array(
'command' => null, 'action' => 'run', 'args' => array()
@@ -29,18 +29,19 @@ class Router extends \lithium\core\Object {
if (!empty($request->params)) {
$params = $request->params + $params;
}
- if (!empty($request->args)) {
- $args = $request->args;
+
+ if (!empty($request->argv)) {
+ $args = $request->argv;
if (empty($params['command'])) {
$params['command'] = array_shift($args);
}
while ($arg = array_shift($args)) {
- if (preg_match('/^-(?P<key>[a-zA-Z0-9]+)$/', $arg, $match)) {
+ if (preg_match('/^-(?P<key>[a-zA-Z0-9]+)$/i', $arg, $match)) {
$params[$match['key']] = true;
continue;
}
- if (preg_match('/^--(?P<key>[a-z0-9-]+)(?:=(?P<val>.+))?$/', $arg, $match)) {
+ if (preg_match('/^--(?P<key>[a-z0-9-]+)(?:=(?P<val>.+))?$/i', $arg, $match)) {
$params[$match['key']] = !isset($match['val']) ? true : $match['val'];
continue;
}
diff --git a/libraries/lithium/console/command/Create.php b/libraries/lithium/console/command/Create.php
index a8e741f..3f1f5e9 100644
--- a/libraries/lithium/console/command/Create.php
+++ b/libraries/lithium/console/command/Create.php
@@ -2,19 +2,24 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command;
-use \lithium\core\Libraries;
-use \lithium\util\String;
+use lithium\util\String;
+use lithium\core\Libraries;
+use lithium\util\Inflector;
+use lithium\core\ClassNotFoundException;
/**
* The `create` command allows you to rapidly develop your models, views, controllers, and tests
* by generating the minimum code necessary to test and run your application.
*
+ * `li3 create --template=controller Posts`
+ * `li3 create --template=model Post`
+ *
*/
class Create extends \lithium\console\Command {
@@ -30,107 +35,239 @@ class Create extends \lithium\console\Command {
/**
* Name of library to use
*
+ * @var string
*/
- public $library = 'app';
+ public $library = null;
/**
- * The template to use to generate the file.
+ * The name of the template to use to generate the file. This allows you to add a custom
+ * template to be used in place of the core template for each command. Place templates in
+ * `<library>\extensions\command\create\template`.
*
+ * @var string
*/
public $template = null;
/**
- * Class Constrcutor.
+ * Holds library data from `lithium\core\Libraries::get()`.
+ *
+ * @var array
+ */
+ protected $_library = array();
+
+ /**
+ * Class initializer. Parses template and sets up params that need to be filled.
*
- * @param string $config
+ * @return void
*/
- public function __construct($config = array()) {
- $this->template = strtolower(join('', array_slice(explode("\\", get_class($this)), -1)));
- parent::__construct($config);
+ protected function _init() {
+ parent::_init();
+ $this->library = $this->library ?: true;
+ $defaults = array('prefix' => null, 'path' => null);
+ $this->_library = (array) Libraries::get($this->library) + $defaults;
}
/**
* Run the create command. Takes `$command` and delegates to `$command::$method`
*
* @param string $command
- * @param string $method
- * @return void
+ * @return boolean
*/
- public function run($command = null, $method = 'run') {
- if (!$command) {
- return $this->interactive();
+ public function run($command = null) {
+ if ($command && !$this->request->args()) {
+ return $this->_default($command);
}
- $class = Libraries::locate('command.create', $command);
- $command = new $class(array(
- 'request' => $this->request->shift(2), 'classes'=> $this->_classes
- ));
+ $this->request->shift();
+ $this->template = $this->template ?: $command;
- if (!method_exists($command, $method)) {
- array_unshift($command->request->params['args'], $method);
- $method = 'run';
+ if (!$command) {
+ $command = $this->in('What would you like to create?', array(
+ 'choices' => array('model', 'view', 'controller', 'test', 'mock')
+ ));
+ }
+ if (!$command) {
+ return false;
+ }
+ if ($this->_execute($command)) {
+ return true;
}
- return $command->invokeMethod($method, $command->request->params['args']);
+ $this->error("{$command} could not be created.");
+ return false;
}
/**
- * Ask questions and use answers to create.
+ * [-i] Ask questions and use answers to create.
*
- * @return void
+ * @return boolean
*/
public function interactive() {
+ $this->i = true;
+ return $this->run();
+ }
+
+ /**
+ * Execute the given sub-command for the current request.
+ *
+ * @param string $command The sub-command name. example: Model, Controller, Test
+ * @param string $params
+ * @return void
+ */
+ protected function _execute($command) {
+ try {
+ if (!$class = $this->_instance($command)) {
+ return false;
+ }
+ } catch (ClassNotFoundException $e) {
+ return false;
+ }
+ $data = array();
+ $params = $class->invokeMethod('_params');
+
+ foreach ($params as $i => $param) {
+ $data[$param] = $class->invokeMethod("_{$param}", array($this->request));
+ }
+ if ($message = $class->invokeMethod('_save', array($data))) {
+ $this->out($message);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Run through the default set. model, controller, test model, test controller
+ *
+ * @param string $name class name to create
+ * @return boolean
+ */
+ protected function _default($name) {
+ $commands = array(
+ array('model', Inflector::classify($name)),
+ array('controller', Inflector::pluralize($name)),
+ array('test', 'model', Inflector::classify($name)),
+ array('test', 'controller', Inflector::pluralize($name))
+ );
+ foreach ($commands as $args) {
+ $command = $this->template = $this->request->params['command'] = array_shift($args);
+ $this->request->params['action'] = array_shift($args);
+ $this->request->params['args'] = $args;
+
+ if (!$this->_execute($command)) {
+ return false;
+ }
+ }
+ return true;
}
/**
* Get the namespace.
*
- * @param string $name
+ * @param string $request
+ * @param array $options
* @return string
*/
- protected function _namespace($name) {
- $nameToSpace = array(
- 'model' => 'models', 'view' => 'views', 'controller' => 'controllers',
- 'command' => 'extensions.command', 'adapter' => 'extensions.adapter',
- 'helper' => 'extensions.helper'
+ protected function _namespace($request, $options = array()) {
+ $name = $request->command;
+ $defaults = array(
+ 'prefix' => $this->_library['prefix'],
+ 'prepend' => null,
+ 'spaces' => array(
+ 'model' => 'models', 'view' => 'views', 'controller' => 'controllers',
+ 'command' => 'extensions.command', 'adapter' => 'extensions.adapter',
+ 'helper' => 'extensions.helper'
+ )
);
- if (isset($nameToSpace[$name])) {
- $name = $nameToSpace[$name];
+ $options += $defaults;
+
+ if (isset($options['spaces'][$name])) {
+ $name = $options['spaces'][$name];
}
- return str_replace('.', '\\', $name);
+ return str_replace('.', '\\', $options['prefix'] . $options['prepend'] . $name);
}
/**
- * Save a template with the current params. Writes file to `Create::$path`.
+ * Parse a template to find available variables specified in `{:name}` format. Each variable
+ * corresponds to a method in the sub command. For example, a `{:namespace}` variable will
+ * call the namespace method in the model command when `li3 create model Post` is called.
*
- * @param string $template
- * @param string $params
- * @return boolean
+ * @return array
+ */
+ protected function _params() {
+ $contents = $this->_template();
+
+ if (empty($contents)) {
+ return array();
+ }
+ preg_match_all('/(?:\{:(?P<params>[^}]+)\})/', $contents, $keys);
+
+ if (!empty($keys['params'])) {
+ return array_values(array_unique($keys['params']));
+ }
+ return array();
+ }
+
+ /**
+ * Returns the contents of the template.
+ *
+ * @return string
*/
- protected function _save($template, $params = array()) {
- $file = Libraries::locate('command.create.template', $template, array(
+ protected function _template() {
+ $file = Libraries::locate('command.create.template', $this->template, array(
'filter' => false, 'type' => 'file', 'suffix' => '.txt.php',
));
if (!$file || is_array($file)) {
return false;
}
+ return file_get_contents($file);
+ }
- $contents = file_get_contents($file);
+ /**
+ * Get an instance of a sub-command
+ *
+ * @param string $name the name of the sub-command to instantiate
+ * @param array $config
+ * @return object;
+ */
+ protected function _instance($name, array $config = array()) {
+ if ($class = Libraries::locate('command.create', Inflector::camelize($name))) {
+ $this->request->params['i'] = $this->i;
+ $this->request->params['template'] = $this->template;
+
+ return new $class(array(
+ 'request' => $this->request,
+ 'classes'=> $this->_classes,
+ ));
+ }
+ return parent::_instance($name, $config);
+ }
+
+
+ /**
+ * Save a template with the current params. Writes file to `Create::$path`.
+ *
+ * @param string $params
+ * @return boolean
+ */
+ protected function _save(array $params = array()) {
+ $defaults = array('namespace' => null, 'class' => null);
+ $params += $defaults;
+
+ if (empty($params['class']) || empty($this->_library['path'])) {
+ return false;
+ }
+ $contents = $this->_template();
$result = String::insert($contents, $params);
- $library = Libraries::get($this->library);
-
- if (!empty($library['path'])) {
- $path = $library['path'] . str_replace(array('\\', $this->library), array('/',''),
- "\\{$params['namespace']}\\{$params['class']}"
- );
- $file = str_replace('//', '/', "{$path}.php");
- $directory = dirname($file);
-
- if (!is_dir($directory)) {
- if (!mkdir($directory, 0755, true)) {
- return false;
- }
- }
- return file_put_contents($file, "<?php\n\n{$result}\n\n?>");
+
+ $path = str_replace('\\', '/', "{$params['namespace']}\\{$params['class']}");
+ $path = $this->_library['path'] . stristr($path, '/');
+ $file = str_replace('//', '/', "{$path}.php");
+ $directory = dirname($file);
+
+ if ((!is_dir($directory)) && !mkdir($directory, 0755, true)) {
+ return false;
+ }
+ if (file_put_contents($file, "<?php\n\n{$result}\n\n?>")) {
+ return "{$params['class']} created in {$params['namespace']}.";
}
return false;
}
diff --git a/libraries/lithium/console/command/G11n.php b/libraries/lithium/console/command/G11n.php
index dd871fe..a3fb1f8 100644
--- a/libraries/lithium/console/command/G11n.php
+++ b/libraries/lithium/console/command/G11n.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command;
-use \lithium\console\command\g11n\Extract;
+use lithium\console\command\g11n\Extract;
/**
* The `G11n` set of commands deals with the extraction and merging of message templates.
diff --git a/libraries/lithium/console/command/Help.php b/libraries/lithium/console/command/Help.php
index 57b9194..9cac262 100644
--- a/libraries/lithium/console/command/Help.php
+++ b/libraries/lithium/console/command/Help.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command;
-use \lithium\core\Libraries;
-use \lithium\util\Inflector;
-use \lithium\analysis\Inspector;
-use \lithium\analysis\Docblock;
+use lithium\core\Libraries;
+use lithium\util\Inflector;
+use lithium\analysis\Inspector;
+use lithium\analysis\Docblock;
/**
* Get information about a particular class including methods, properties, and descriptions.
@@ -27,31 +27,30 @@ class Help extends \lithium\console\Command {
*/
public function run($name = null) {
if (!$name) {
- $this->nl();
- $this->out('COMMANDS', 'heading1');
+ $this->out('COMMANDS', 'heading1', 2);
$commands = Libraries::locate('command', null, array('recursive' => false));
foreach ($commands as $command) {
$info = Inspector::info($command);
- $this->out($this->_pad(Inflector::classify($info['shortName'])), 'heading2');
- $this->out($this->_pad(strtok($info['description'], "\n"), 2));
- $this->nl();
+ $name = strtolower(Inflector::slug($info['shortName']));
+ $this->out($this->_pad($name), 'heading2');
+ $this->out($this->_pad($info['description']), 2);
}
- $this->out(
- 'See `{:command}li3 help COMMAND{:end}` '
- . 'for more information on a specific command.'
- );
+ $message = 'See `{:command}li3 help COMMAND{:end}`';
+ $message .= ' for more information on a specific command.';
+ $this->out($message, 2);
return true;
}
- $class = Libraries::locate('command', $name);
+ $name = Inflector::classify($name);
- if (!$class) {
+ if (!$class = Libraries::locate('command', $name)) {
$this->error("{$name} not found");
return false;
}
if (strpos($name, '\\') !== false) {
$name = join('', array_slice(explode("\\", $name), -1));
}
+ $name = strtolower(Inflector::slug($name));
$methods = $this->_methods($class);
$properties = $this->_properties($class);
diff --git a/libraries/lithium/console/command/Library.php b/libraries/lithium/console/command/Library.php
index 81a92e1..da4dbbe 100644
--- a/libraries/lithium/console/command/Library.php
+++ b/libraries/lithium/console/command/Library.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command;
-use \Phar;
-use \RuntimeException;
-use \lithium\core\Libraries;
+use Phar;
+use Exception;
+use RuntimeException;
+use lithium\core\Libraries;
/**
* The Library command is used to archive and extract Phar::GZ archives. Requires zlib extension.
@@ -92,11 +93,20 @@ class Library extends \lithium\console\Command {
/**
* some classes
*
- * @package default
+ * @var array
*/
protected $_classes = array(
- 'service' => '\lithium\net\http\Service',
- 'response' => '\lithium\console\Response'
+ 'service' => 'lithium\net\http\Service',
+ 'response' => 'lithium\console\Response'
+ );
+
+ /**
+ * Auto configuration properties.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array(
+ 'classes' => 'merge', 'env', 'detectors' => 'merge', 'base', 'type', 'stream'
);
/**
@@ -110,14 +120,11 @@ class Library extends \lithium\console\Command {
*/
protected function _init() {
parent::_init();
-
- if (!isset($this->request)) {
- throw new RuntimeException("Command has been initialized without a request object");
+ if ($this->server) {
+ $this->_settings['servers'][$this->server] = true;
}
-
- $this->_settings['servers'][$this->server] = true;
if (file_exists($this->conf)) {
- $this->_settings += json_decode($this->conf, true);
+ $this->_settings += (array) json_decode($this->conf, true);
}
$this->path = $this->_toPath($this->path ?: 'libraries');
$this->force = $this->f ? $this->f : $this->force;
@@ -133,7 +140,7 @@ class Library extends \lithium\console\Command {
*/
public function config($key = null, $value = null, $options = true) {
if (empty($key) || empty($value)) {
- return false;
+ return $this->_settings;
}
switch($key) {
case 'server':
@@ -178,7 +185,7 @@ class Library extends \lithium\console\Command {
if (file_exists($from)) {
try {
$archive = new Phar($from);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$this->error($e->getMessage());
return false;
}
@@ -207,7 +214,7 @@ class Library extends \lithium\console\Command {
*/
public function archive($name = null, $result = null) {
if (ini_get('phar.readonly') == '1') {
- throw new RuntimeException('set phar.readonly = 0 in php.ini');
+ throw new RuntimeException('Set `phar.readonly` to `0` in `php.ini`.');
}
$from = $name;
$to = $name;
@@ -227,13 +234,16 @@ class Library extends \lithium\console\Command {
}
try {
$archive = new Phar("{$path}.phar");
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$this->error($e->getMessage());
return false;
}
+ $result = null;
$from = $this->_toPath($from);
- $result = (boolean) $archive->buildFromDirectory($from, $this->filter);
+ if (is_dir($from)) {
+ $result = (boolean) $archive->buildFromDirectory($from, $this->filter);
+ }
if (file_exists("{$path}.phar.gz")) {
if (!$this->force) {
$this->error(basename($path) . ".phar.gz already exists in " . dirname($path));
@@ -262,7 +272,7 @@ class Library extends \lithium\console\Command {
foreach ($this->_settings['servers'] as $server => $enabled) {
if (!$enabled) { continue; }
- $service = new $this->_classes['service'](array(
+ $service = $this->_instance('service', array(
'host' => $server, 'port' => $this->port
));
$results[$server] = json_decode($service->get("lab/{$type}.json"));
@@ -272,16 +282,13 @@ class Library extends \lithium\console\Command {
continue;
}
foreach ((array) $results[$server] as $data) {
- $header = "{$server} > {$data->name}";
-
- if (!$header) {
- $header = "{$server} > {$data->class}";
- }
+ $name = isset($data->class) ? $data->class : $data->name;
+ $header = "{$server} > {$name}";
$out = array(
"{$data->summary}",
- "Version: {$data->version}", "Created: {$data->created}",
+ "Version: {$data->version}",
+ "Created: {$data->created}",
);
-
$this->header($header);
$this->out(array_filter($out));
}
@@ -299,23 +306,22 @@ class Library extends \lithium\console\Command {
$results = array();
foreach ($this->_settings['servers'] as $server => $enabled) {
if (!$enabled) { continue; }
- $service = new $this->_classes['service'](array(
+ $service = $this->_instance('service', array(
'host' => $server, 'port' => $this->port
));
- $results[$server] = json_decode($service->get("lab/{$name}.json"));
- }
- if (count($results)) {
- $plugin = current($results);
+ if ($plugin = json_decode($service->get("lab/{$name}.json"))) {
+ break;
+ }
}
if (empty($plugin->sources)) {
- $this->error("{$name} not found");
+ $this->error("{$name} not found.");
return false;
}
$hasGit = function () {
- return (strpos(`git --version`, '1.6') !== false);
+ return (strpos(shell_exec('git --version'), 'git version') !== false);
};
foreach ((array) $plugin->sources as $source) {
- if (strpos($source, 'phar.gz') !== false) {
+ if (strpos($source, 'phar.gz') !== false && file_exists($source)) {
$written = file_put_contents(
"{$this->path}/{$plugin->name}.phar.gz", file_get_contents($source)
);
@@ -324,14 +330,16 @@ class Library extends \lithium\console\Command {
return false;
}
$this->out("{$plugin->name}.phar.gz saved to {$this->path}");
+
try {
$archive = new Phar("{$this->path}/{$plugin->name}.phar.gz");
+
if ($archive->extractTo("{$this->path}/{$plugin->name}")) {
$this->out("{$plugin->name} installed to {$this->path}/{$plugin->name}");
$this->out("Remember to update the bootstrap.");
return true;
}
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$this->error($e->getMessage());
}
}
@@ -339,8 +347,13 @@ class Library extends \lithium\console\Command {
if (!empty($url['scheme']) && $url['scheme'] == 'git' && $hasGit()) {
$result = shell_exec(
- "cd {$this->path} && git clone {$source}"
+ "cd {$this->path} && git clone {$source} {$plugin->name}"
);
+ if (is_dir("{$this->path}/{$plugin->name}")) {
+ $this->out("{$plugin->name} installed to {$this->path}/{$plugin->name}");
+ $this->out("Remember to update the bootstrap.");
+ return true;
+ }
}
}
$this->out("{$plugin->name} not installed.");
@@ -359,8 +372,9 @@ class Library extends \lithium\console\Command {
}
$result = false;
$path = $this->_toPath($name);
- $name = basename($name);
+ $name = basename($path);
$formula = "{$path}/config/{$name}.json";
+
$data = array();
if (file_exists($formula)) {
@@ -373,7 +387,7 @@ class Library extends \lithium\console\Command {
$data['summary'] = $this->in("please supply a summary");
}
if (file_exists($path) && !file_exists($formula)) {
- $data = json_encode($data + array(
+ $defaults = array(
'name' => $name, 'version' => '0.1',
'summary' => "a plugin called {$name}",
'maintainers' => array(array(
@@ -384,14 +398,15 @@ class Library extends \lithium\console\Command {
'install' => array(), 'update' => array(), 'remove' => array(),
),
'requires' => array()
- ));
+ );
+ $data += $defaults;
+
if (!is_dir(dirname($formula)) && !mkdir(dirname($formula), 0755, true)) {
$this->error("Formula for {$name} not created in {$path}");
return false;
}
- $result = file_put_contents($formula, $data);
}
- if ($result) {
+ if (is_dir(dirname($formula)) && file_put_contents($formula, json_encode($data))) {
$this->out("Formula for {$name} created in {$path}.");
return true;
}
@@ -431,9 +446,9 @@ class Library extends \lithium\console\Command {
return false;
}
if (file_exists($file)) {
- $service = new $this->_classes['service'](array(
+ $service = $this->_instance('service', array(
'host' => $this->server, 'port' => $this->port,
- 'login' => $this->username, 'password' => $this->password
+ 'auth' => 'Basic', 'username' => $this->username, 'password' => $this->password
));
$boundary = md5(date('r', time()));
$headers = array("Content-Type: multipart/form-data; boundary={$boundary}");
@@ -445,7 +460,10 @@ class Library extends \lithium\console\Command {
base64_encode(file_get_contents($file)),
"--{$boundary}--"
));
- $result = json_decode($service->post('/lab/server/receive', $data, compact('headers')));
+ $result = json_decode($service->post(
+ '/lab/server/receive', $data, compact('headers')
+ ));
+
if ($service->last->response->status['code'] == 201) {
$this->out(array(
"{$result->name} added to {$this->server}.",
@@ -481,7 +499,7 @@ class Library extends \lithium\console\Command {
* @return string
*/
protected function _toPath($name = null) {
- if ($name[0] === '/') {
+ if ($name && $name[0] === '/') {
return $name;
}
$library = Libraries::get($name);
diff --git a/libraries/lithium/console/command/Route.php b/libraries/lithium/console/command/Route.php
new file mode 100644
index 0000000..86fd586
--- /dev/null
+++ b/libraries/lithium/console/command/Route.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+namespace lithium\console\command;
+
+use lithium\net\http\Router;
+use lithium\action\Request;
+use lithium\core\Environment;
+
+/**
+ * The route command lets you inspect your routes and issue requests against the router.
+ */
+class Route extends \lithium\console\Command {
+
+ /**
+ * Override the default 'development' environment.
+ *
+ * For example:
+ * {{{
+ * li3 route --env=production
+ * li3 route show /foo --env=test
+ * }}}
+ *
+ * @var string
+ */
+ public $env = 'development';
+
+ /**
+ * Load the routes file and set the environment.
+ *
+ * @param array $config The default configuration, wherein the absolute path to the routes file
+ * to load may be specified, using the `'routes_file'` key.
+ */
+ public function __construct($config = array()) {
+ $defaults = array('routes_file' => LITHIUM_APP_PATH . '/config/routes.php');
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+ Environment::set($this->env);
+
+ if (file_exists($this->_config['routes_file'])) {
+ return require $this->_config['routes_file'];
+ }
+ $this->error("The routes file for this library doesn't exist or can't be found.");
+ }
+
+ /**
+ * Lists all connected routes to the router. See the `all()`
+ * method for details and examples.
+ *
+ * @return void
+ */
+ public function run() {
+ $this->all();
+ }
+
+ /**
+ * Lists all connected routes to the router. This is a convenience
+ * alias for the `show()` method.
+ *
+ * Example:
+ * {{{
+ * li3 route
+ * li3 route all
+ * }}}
+ *
+ * Will return an output similar to:
+ *
+ * {{{
+ * Template Params
+ * -------- ------
+ * / {"controller":"pages","action":"view"}
+ * /pages/{:args} {"controller":"pages","action":"view"}
+ * /{:slug:[\w\-]+} {"controller":"posts","action":"show"}
+ * /{:controller}/{:action}/{:args} {"action":"index"}
+ * }}}
+ *
+ * @return void
+ */
+ public function all() {
+ $routes = Router::get();
+ $columns = array(array('Template', 'Params'), array('--------', '------'));
+
+ foreach ($routes As $route) {
+ $info = $route->export();
+ $columns[] = array($info['template'], json_encode($info['params']));
+ }
+ $this->columns($columns);
+ }
+
+ /**
+ * Returns the corresponding params for a given URL and an optional request
+ * method.
+ *
+ * Examples:
+ * {{{
+ * 1: li3 route show /foo
+ * 2: li3 route show post /foo/bar/1
+ * 3: li3 route show /test
+ * 4: li3 route show /test --env=production
+ * }}}
+ *
+ * Will return outputs similar to:
+ *
+ * {{{
+ * 1: {"controller":"foo","action":"index" }
+ * 2: {"controller":"foo","action":"bar","args":["1"]}
+ * 3: {"controller":"lithium\\test\\Controller","action":"index"}
+ * 4: {"controller":"test","action":"index"}
+ * }}}
+ *
+ * @return void
+ */
+ public function show() {
+ $url = join(" ", $this->request->params['args']);
+ $method = 'GET';
+
+ if (!$url) {
+ $this->error('Please provide a valid URL');
+ }
+
+ if (preg_match('/^(GET|POST|PUT|DELETE|HEAD|OPTIONS) (.+)/i', $url, $matches)) {
+ $method = strtoupper($matches[1]);
+ $url = $matches[2];
+ }
+
+ $request = new Request(compact('url') + array('env' => array('REQUEST_METHOD' => $method)));
+ $result = Router::process($request);
+ $this->out($result->params ? json_encode($result->params) : "No route found.");
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/command/Test.php b/libraries/lithium/console/command/Test.php
index 07f85c0..3c9ef46 100644
--- a/libraries/lithium/console/command/Test.php
+++ b/libraries/lithium/console/command/Test.php
@@ -2,16 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command;
-use \lithium\core\Libraries;
-use \lithium\test\Group;
-use \lithium\test\Dispatcher;
-use \lithium\analysis\Inspector;
+use lithium\core\Libraries;
+use lithium\test\Group;
+use lithium\test\Dispatcher;
/**
* Runs a given set of tests and outputs the results.
@@ -68,7 +67,8 @@ class Test extends \lithium\console\Command {
* lithium test --group=lithium.tests.cases.core
* }}}
*
- * @return void
+ * @param string $path Absolute or relative path to tests.
+ * @return boolean Will exit with status `1` if one or more tests failed otherwise with `0`.
*/
public function run() {
$this->header('Test');
@@ -76,29 +76,32 @@ class Test extends \lithium\console\Command {
if ($this->_getTests() != true) {
return 0;
}
- $startBenchmark = microtime(true);
-
error_reporting(E_ALL | E_STRICT | E_DEPRECATED);
- if (!empty($this->case)) {
- $this->case = '\\' . str_replace('.', '\\', $this->case);
- } elseif (!empty($this->group)) {
- $this->group = '\\' . str_replace('.', '\\', $this->group);
- }
$run = $this->case ?: $this->group;
- $this->nl();
+ $run = '\\' . str_replace('.', '\\', $run);
$this->out(sprintf('Running `%s`... ', $run), false);
$report = Dispatcher::run($run, array(
- 'filters' => $this->filters, 'reporter' => 'console'
+ 'filters' => $this->filters,
+ 'reporter' => 'console',
+ 'format' => 'txt'
));
+ $stats = $report->stats();
+
$this->out('done.', 2);
+ $this->out('{:heading1}Results{:end}', 0);
+ $this->out($report->render('stats', $stats));
- if ($output = $report->filters()) {
- $this->out($output, 2);
+ foreach ($report->filters() as $filter => $options) {
+ $data = $report->results['filters'][$filter];
+ $this->out($report->render($options['name'], compact('data')));
}
- $this->out($report->stats());
+
+ $this->hr();
$this->nl();
+
+ return $stats['success'];
}
/**
@@ -111,9 +114,12 @@ class Test extends \lithium\console\Command {
$classes = Libraries::find(true, array(
'recursive' => true,
- 'exclude' => '/\w+Test$|webroot|index$|^app\\\\config|^app\\\\views/'
+ 'exclude' => '/tests|resources|webroot|index$|^app\\\\config|^app\\\\views/'
));
- $tests = Group::all();
+ $map = function($t) {
+ return str_replace('\tests\cases', '', preg_replace('/Test$/', '', $t));
+ };
+ $tests = array_map($map, Group::all());
$classes = array_diff($classes, $tests);
sort($classes);
diff --git a/libraries/lithium/console/command/create/Controller.php b/libraries/lithium/console/command/create/Controller.php
index db57cdf..cff5aa5 100644
--- a/libraries/lithium/console/command/create/Controller.php
+++ b/libraries/lithium/console/command/create/Controller.php
@@ -2,41 +2,82 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command\create;
-use \lithium\core\Libraries;
-use \lithium\util\Inflector;
+use lithium\util\Inflector;
+/**
+ * Generate a Controller class in the `--library` namespace
+ *
+ * `li3 create controller Posts`
+ * `li3 create --library=li3_plugin controller Posts`
+ *
+ */
class Controller extends \lithium\console\command\Create {
- public function run($name = null, $null = null) {
- $library = Libraries::get($this->library);
- if (empty($library['prefix'])) {
- return false;
- }
- $model = Inflector::classify($name);
- $use = "\\{$library['prefix']}models\\{$model}";
-
- $params = array(
- 'namespace' => "{$library['prefix']}controllers",
- 'use' => $use,
- 'class' => "{$name}Controller",
- 'model' => $model,
- 'singular' => Inflector::singularize(Inflector::underscore($name)),
- 'plural' => Inflector::pluralize(Inflector::underscore($name))
- );
-
- if ($this->_save($this->template, $params)) {
- $this->out(
- "{$params['class']} created in {$params['namespace']}."
- );
- return true;
- }
- return false;
+ /**
+ * Get the fully-qualified model class that is used by the controller.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _use($request) {
+ $request->params['command'] = 'model';
+ return $this->_namespace($request) . '\\' . $this->_model($request);
+ }
+
+ /**
+ * Get the controller class name.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _class($request) {
+ return $this->_name($request) . 'Controller';
+ }
+
+ /**
+ * Returns the name of the controller class, minus `'Controller'`.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _name($request) {
+ return Inflector::camelize(Inflector::pluralize($request->action));
+ }
+
+ /**
+ * Get the plural variable used for data in controller methods.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _plural($request) {
+ return Inflector::pluralize(Inflector::camelize($request->action, false));
+ }
+
+ /**
+ * Get the model class used in controller methods.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _model($request) {
+ return Inflector::classify($request->action);
+ }
+
+ /**
+ * Get the singular variable to use for data in controller methods.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _singular($request) {
+ return Inflector::singularize(Inflector::camelize($request->action, false));
}
}
diff --git a/libraries/lithium/console/command/create/Mock.php b/libraries/lithium/console/command/create/Mock.php
new file mode 100644
index 0000000..44c13dc
--- /dev/null
+++ b/libraries/lithium/console/command/create/Mock.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\command\create;
+
+use lithium\util\Inflector;
+
+/**
+ * Generate a Mock that extends the name of the given class in the `--library` namespace.
+ *
+ * `li3 create mock model Post`
+ * `li3 create --library=li3_plugin mock model Post`
+ *
+ */
+class Mock extends \lithium\console\command\Create {
+
+ /**
+ * Get the namespace for the mock.
+ *
+ * @param string $request
+ * @param string $options
+ * @return string
+ */
+ protected function _namespace($request, $options = array()) {
+ $request->params['command'] = $request->action;
+ return parent::_namespace($request, array('prepend' => 'tests.mocks.'));
+ }
+
+ /**
+ * Get the parent for the mock.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _parent($request) {
+ $namespace = parent::_namespace($request);
+ $class = $request->action;
+ return "\\{$namespace}\\{$class}";
+ }
+
+ /**
+ * Get the class name for the mock.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _class($request) {
+ $type = $request->action;
+ $name = $request->args();
+
+ if ($command = $this->_instance($type)) {
+ $request->params['action'] = $name;
+ $name = $command->invokeMethod('_class', array($request));
+ }
+ return Inflector::classify("Mock{$name}");
+ }
+
+ /**
+ * Get the methods for the mock to override
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _methods($request) {
+ return null;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/command/create/Model.php b/libraries/lithium/console/command/create/Model.php
index 2f0574b..4bdefc8 100644
--- a/libraries/lithium/console/command/create/Model.php
+++ b/libraries/lithium/console/command/create/Model.php
@@ -2,33 +2,31 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command\create;
-use \lithium\core\Libraries;
+use lithium\util\Inflector;
+/**
+ * Generate a Model class in the `--library` namespace
+ *
+ * `li3 create mdoel Post`
+ * `li3 create --library=li3_plugin model Post`
+ *
+ */
class Model extends \lithium\console\command\Create {
- public function run($name = null, $null = null) {
- $library = Libraries::get($this->library);
- if (empty($library['prefix'])) {
- return false;
- }
- $params = array(
- 'namespace' => "{$library['prefix']}models",
- 'class' => "{$name}",
- );
-
- if ($this->_save($this->template, $params)) {
- $this->out(
- "{$params['class']} created in {$params['namespace']}."
- );
- return true;
- }
- return false;
+ /**
+ * Get the class name for the model.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _class($request) {
+ return Inflector::classify($request->action);
}
}
diff --git a/libraries/lithium/console/command/create/Test.php b/libraries/lithium/console/command/create/Test.php
index 1c60542..cc52fcc 100644
--- a/libraries/lithium/console/command/create/Test.php
+++ b/libraries/lithium/console/command/create/Test.php
@@ -2,91 +2,103 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command\create;
-use \lithium\core\Libraries;
-use \lithium\analysis\Inspector;
+use lithium\core\Libraries;
+use lithium\util\Inflector;
+use lithium\analysis\Inspector;
+use lithium\core\ClassNotFoundException;
/**
- * Create test cases or mocks in a namespace for a class.
+ * Generate a Test class in the `--library` namespace
+ *
+ * `li3 create test model Post`
+ * `li3 create --library=li3_plugin test model Post`
*
*/
class Test extends \lithium\console\command\Create {
- /**
- * Generate test cases in the given namespace.
- * `li3 create test model Post`
- * `li3 create test --library=li3_plugin model Post`
- *
- * @param string $type namespace of the class (e.g. model, controller, some.name.space).
- * @param string $name Name of class to test.
- * @return void
- */
- public function run($type = null, $name = null) {
- $library = Libraries::get($this->library);
- if (empty($library['prefix'])) {
- return false;
- }
- $namespace = $this->_namespace($type);
- $use = "\\{$library['prefix']}{$namespace}\\{$name}";
- $methods = array();
+ /**
+ * Get the namespace for the test case.
+ *
+ * @param string $request
+ * @param array $options
+ * @return string
+ */
+ protected function _namespace($request, $options = array()) {
+ $request->params['command'] = $request->action;
+ return parent::_namespace($request, array('prepend' => 'tests.cases.'));
+ }
+
+ /**
+ * Get the class used by the test case.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _use($request) {
+ return parent::_namespace($request) . '\\' . $this->_name($request);
+ }
- if (class_exists($use, false)) {
- $methods = array();
- foreach (array_keys(Inspector::methods($use, 'extents')) as $method) {
- $methods[] = "\tpublic function test" . ucwords($method) . "() {}";
- }
+ /**
+ * Get the class name for the test case.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _class($request) {
+ $name = $this->_name($request);
+ return Inflector::classify("{$name}Test");
+ }
+
+ /**
+ * Get the methods to test.
+ *
+ * @param string $request
+ * @return string
+ */
+ protected function _methods($request) {
+ $use = $this->_use($request);
+ $path = Libraries::path($use);
+
+ if (!file_exists($path)) {
+ return "";
}
- $params = array(
- 'namespace' => "{$library['prefix']}tests\\cases\\{$namespace}",
- 'use' => $use,
- 'class' => "{$name}Test",
- 'methods' => join("\n", $methods),
- );
+ $methods = (array) Inspector::methods($use, 'extents');
+ $testMethods = array();
- if ($this->_save($this->template, $params)) {
- $this->out(
- "{$params['class']} created for {$name} in {$params['namespace']}."
- );
- return true;
+ foreach (array_keys($methods) as $method) {
+ $testMethods[] = "\tpublic function test" . ucwords($method) . "() {}";
}
- return false;
+ return join("\n", $testMethods);
}
/**
- * Generate a Mock that extends the name of the given class in the given namespace.
- * `li3 create test mock model Post`
- * `li3 create test --library=li3_plugin mock model Post`
+ * Get the class to be tested
*
- * @param string $type namespace of the class (e.g. model, controller, some.name.space).
- * @param string $name Class name to extend with the mock.
- * @return void
+ * @param string $request
+ * @return string
*/
- public function mock($type = null, $name = null) {
- $library = Libraries::get($this->library);
- if (empty($library['prefix'])) {
- return false;
- }
- $namespace = $this->_namespace($type);
- $params = array(
- 'namespace' => "{$library['prefix']}tests\\mocks\\{$namespace}",
- 'class' => "Mock{$name}",
- 'parent' => "\\{$library['prefix']}{$namespace}\\{$name}",
- 'methods' => null
- );
- if ($this->_save('mock', $params)) {
- $this->out("{$params['class']} created for {$name} in {$params['namespace']}.");
- return true;
- }
- return false;
- }
+ protected function _name($request) {
+ $type = $request->action;
+ $name = $request->args();
- public function interactive() {
+ try {
+ $command = $this->_instance($type);
+ } catch (ClassNotFoundException $e) {
+ $command = null;
+ }
+ if ($command) {
+ $request->params['action'] = $name;
+ $name = $command->invokeMethod('_class', array($request));
+ }
+ $request->params['action'] = $type;
+ return $name;
}
}
diff --git a/libraries/lithium/console/command/create/View.php b/libraries/lithium/console/command/create/View.php
index 9354e7b..7a89487 100644
--- a/libraries/lithium/console/command/create/View.php
+++ b/libraries/lithium/console/command/create/View.php
@@ -2,12 +2,54 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command\create;
-class View extends \lithium\console\command\Create {}
+use lithium\util\Inflector;
+use lithium\util\String;
+
+/**
+ * Generate a View file in the `--library` namespace
+ *
+ * `li3 create view Posts index`
+ * `li3 create --library=li3_plugin view Posts index`
+ *
+ */
+class View extends \lithium\console\command\Create {
+
+ /**
+ * Override the save method to handle view specific params.
+ *
+ * @param array $params
+ */
+ protected function _save(array $params = array()) {
+ $params['path'] = Inflector::underscore($this->request->action);
+ $params['file'] = $this->request->args(0);
+
+ $contents = $this->_template();
+ $result = String::insert($contents, $params);
+
+ if (!empty($this->_library['path'])) {
+ $path = $this->_library['path'] . "/views/{$params['path']}/{$params['file']}";
+ $file = str_replace('//', '/', "{$path}.php");
+ $directory = dirname($file);
+
+ if (!is_dir($directory)) {
+ if (!mkdir($directory, 0755, true)) {
+ return false;
+ }
+ }
+ $directory = str_replace($this->_library['path'] . '/', '', $directory);
+
+ if (file_put_contents($file, "<?php\n\n{$result}\n\n?>")) {
+ return "{$params['file']}.php created in {$directory}.";
+ }
+ }
+ return false;
+ }
+}
?>
\ No newline at end of file
diff --git a/libraries/lithium/console/command/create/template/app.phar.gz b/libraries/lithium/console/command/create/template/app.phar.gz
index cce5e2f..7357e23 100644
Binary files a/libraries/lithium/console/command/create/template/app.phar.gz and b/libraries/lithium/console/command/create/template/app.phar.gz differ
diff --git a/libraries/lithium/console/command/create/template/controller.txt.php b/libraries/lithium/console/command/create/template/controller.txt.php
index 512941e..feb4077 100644
--- a/libraries/lithium/console/command/create/template/controller.txt.php
+++ b/libraries/lithium/console/command/create/template/controller.txt.php
@@ -9,39 +9,28 @@ class {:class} extends \lithium\action\Controller {
return compact('{:plural}');
}
- public function view($id = null) {
- ${:singular} = {:model}::find($id);
+ public function view() {
+ ${:singular} = {:model}::first($this->request->id);
return compact('{:singular}');
}
public function add() {
- if (!empty($this->request->data)) {
- ${:singular} = {:model}::create($this->request->data);
- if (${:singular}->save()) {
- $this->redirect(array(
- 'controller' => '{:plural}', 'action' => 'view',
- 'args' => array(${:singular}->id)
- ));
- }
- }
- if (empty(${:singular})) {
- ${:singular} = {:model}::create();
+ ${:singular} = {:model}::create();
+
+ if (($this->request->data) && ${:singular}->save($this->request->data)) {
+ $this->redirect(array('{:name}::view', 'args' => array(${:singular}->id)));
}
return compact('{:singular}');
}
- public function edit($id = null) {
- ${:singular} = {:model}::find($id);
- if (empty(${:singular})) {
- $this->redirect(array('controller' => '{:plural}', 'action' => 'index'));
+ public function edit() {
+ ${:singular} = {:model}::find($this->request->id);
+
+ if (!${:singular}) {
+ $this->redirect('{:name}::index');
}
- if (!empty($this->request->data)) {
- if (${:singular}->save($this->request->data)) {
- $this->redirect(array(
- 'controller' => '{:plural}', 'action' => 'view',
- 'args' => array(${:singular}->id)
- ));
- }
+ if (($this->request->data) && ${:singular}->save($this->request->data)) {
+ $this->redirect(array('{:name}::view', 'args' => array(${:singular}->id)));
}
return compact('{:singular}');
}
diff --git a/libraries/lithium/console/command/g11n/Extract.php b/libraries/lithium/console/command/g11n/Extract.php
index b94e3b6..6a22e2c 100644
--- a/libraries/lithium/console/command/g11n/Extract.php
+++ b/libraries/lithium/console/command/g11n/Extract.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\console\command\g11n;
-use \Exception;
-use \DateTime;
-use \lithium\g11n\Catalog;
+use DateTime;
+use Exception;
+use lithium\g11n\Catalog;
+use lithium\core\Libraries;
/**
* The `Extract` class is a command for extracting messages from files.
@@ -26,7 +27,7 @@ class Extract extends \lithium\console\Command {
public function _init() {
parent::_init();
$this->source = $this->source ?: LITHIUM_APP_PATH;
- $this->destination = $this->destination ?: LITHIUM_APP_PATH . '/resources/g11n';
+ $this->destination = $this->destination ?: Libraries::get(true, 'resources') . '/g11n';
}
/**
@@ -38,20 +39,20 @@ class Extract extends \lithium\console\Command {
$this->header('Message Extraction');
if (!$data = $this->_extract()) {
- $this->err('Yielded no items.');
+ $this->error('Yielded no items.');
return 1;
}
$count = count($data);
- $this->out("Yielded {$count} items.");
- $this->nl();
+ $this->out("Yielded {$count} item(s).");
+ $this->out();
$this->header('Message Template Creation');
if (!$this->_writeTemplate($data)) {
- $this->err('Failed to write template.');
+ $this->error('Failed to write template.');
return 1;
}
- $this->nl();
+ $this->out();
return 0;
}
@@ -65,9 +66,9 @@ class Extract extends \lithium\console\Command {
protected function _extract() {
$message[] = 'A `Catalog` class configuration with an adapter that is capable of';
$message[] = 'handling read requests for the `messageTemplate` category is needed';
- $message[] = 'in order to proceed.';
+ $message[] = 'in order to proceed. This may also be referred to as `extractor`.';
$this->out($message);
- $this->nl();
+ $this->out();
$configs = (array) Catalog::config();
@@ -75,31 +76,29 @@ class Extract extends \lithium\console\Command {
foreach ($configs as $name => $config) {
$this->out(" - {$name}");
}
- $this->nl();
+ $this->out();
$name = $this->in('Please choose a configuration or hit [enter] to add one:', array(
'choices' => array_keys($configs)
));
if (!$name) {
- $adapter = $this->in('Adapter:', array(
- 'default' => 'Gettext'
- ));
- $path = $this->in('Path:', array(
- 'default' => $this->destination
- ));
- $scope = $this->in('Scope:', array(
- 'default' => $this->scope
- ));
- $name = 'runtime' . uniqid();
+ $adapter = $this->in('Adapter:', array('default' => 'Code'));
+ $path = $this->in('Path:', array('default' => $this->source));
+ $scope = $this->in('Scope:', array('default' => $this->scope));
+ $name = 'runtime' . uniqid();
$configs[$name] = compact('adapter', 'path', 'scope');
}
Catalog::config($configs);
- return Catalog::read('messageTemplate', 'root', compact('name') + array(
- 'scope' => $configs[$name]['scope'],
- 'lossy' => false
- ));
+ try {
+ return Catalog::read($name, 'messageTemplate', 'root', array(
+ 'scope' => $configs[$name]['scope'],
+ 'lossy' => false,
+ ));
+ } catch (Exception $e) {
+ return false;
+ }
}
/**
@@ -111,10 +110,10 @@ class Extract extends \lithium\console\Command {
protected function _writeTemplate($data) {
$message[] = 'In order to proceed you need to choose a `Catalog` configuration';
$message[] = 'which is used for writing the template. The adapter for the configuration';
- $message[] = 'should be capable of handling write requests for the `message.template`';
+ $message[] = 'should be capable of handling write requests for the `messageTemplate`';
$message[] = 'category.';
$this->out($message);
- $this->nl();
+ $this->out();
$configs = (array) Catalog::config();
@@ -122,39 +121,38 @@ class Extract extends \lithium\console\Command {
foreach ($configs as $name => $config) {
$this->out(" - {$name}");
}
- $this->nl();
+ $this->out();
$name = $this->in('Please choose a configuration or hit [enter] to add one:', array(
'choices' => array_keys($configs)
));
if (!$name) {
- $adapter = $this->in('Adapter:', array(
- 'default' => 'Gettext'
- ));
- $path = $this->in('Path:', array(
- 'default' => $this->destination
- ));
- $scope = $this->in('Scope:', array(
- 'default' => $this->scope
- ));
- $name = 'runtime' . uniqid();
+ $adapter = $this->in('Adapter:', array('default' => 'Gettext'));
+ $path = $this->in('Path:', array('default' => $this->destination));
+ $scope = $this->in('Scope:', array('default' => $this->scope));
+ $name = 'runtime' . uniqid();
$configs[$name] = compact('adapter', 'path', 'scope');
Catalog::config($configs);
+ } else {
+ $scope = $this->in('Scope:', array('default' => $this->scope));
}
- $scope = $configs[$name]['scope'] ?: $this->in('Scope:', array('default' => $this->scope));
$message = array();
$message[] = 'The template is now ready to be saved.';
$message[] = 'Please note that an existing template will be overwritten.';
$this->out($message);
- $this->nl();
+ $this->out();
- if ($this->in('Save?', array('choices' => array('y', 'n'), 'default' => 'n')) != 'y') {
+ if ($this->in('Save?', array('choices' => array('y', 'n'), 'default' => 'y')) != 'y') {
$this->out('Aborting upon user request.');
$this->stop(1);
}
- return Catalog::write('messageTemplate', 'root', $data, compact('name', 'scope'));
+ try {
+ return Catalog::write($name, 'messageTemplate', 'root', $data, compact('scope'));
+ } catch (Exception $e) {
+ return false;
+ }
}
}
diff --git a/libraries/lithium/console/li3 b/libraries/lithium/console/li3
index 9d784e0..563742c 100755
--- a/libraries/lithium/console/li3
+++ b/libraries/lithium/console/li3
@@ -1,8 +1,9 @@
-#!/bin/bash
+#!/usr/bin/env php
+<?php
#
# Lithium: the most rad php framework
#
# @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
# @license http://opensource.org/licenses/bsd-license.php The BSD License
#
-php -f ${0/%li3/}lithium.php "$@"
+include __DIR__ . '/lithium.php';
diff --git a/libraries/lithium/console/lithium.php b/libraries/lithium/console/lithium.php
index a9a7922..adaa3b0 100644
--- a/libraries/lithium/console/lithium.php
+++ b/libraries/lithium/console/lithium.php
@@ -2,24 +2,22 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
-namespace lithium;
-
-use \lithium\core\Libraries;
-use \lithium\console\Dispatcher;
-
/**
- * Determine if we're in an application context by moving up the directory tree looking for
- * a `config` directory with a `bootstrap.php` file in it. If no application context is found,
- * just boot up the core framework.
+ * This console front-controller file is the gateway to your application
+ * through the command line. It is responsible for intercepting requests, and
+ * handing them off to the `Dispatcher` for processing.
+ *
+ * Determine if we're in an application context by moving up the directory tree
+ * looking for a `config` directory with a `bootstrap.php` file in it. If no
+ * application context is found, just boot up the core framework.
*/
-
$library = dirname(dirname(__DIR__));
-$app = null;
$working = getcwd() ?: __DIR__;
+$app = null;
while (!$app && $working) {
if (file_exists($working . '/config/bootstrap.php')) {
@@ -31,21 +29,54 @@ while (!$app && $working) {
}
}
+if ($app && is_dir("{$app}/config/bootstrap") && file_exists("{$app}/webroot/index.php")) {
+ include "{$app}/config/bootstrap.php";
+ exit(lithium\console\Dispatcher::run(new lithium\console\Request())->status);
+}
+
+define('LITHIUM_LIBRARY_PATH', $library);
+define('LITHIUM_APP_PATH', $app ? $working : dirname($library) . '/app');
+
+if (!include LITHIUM_LIBRARY_PATH . '/lithium/core/Libraries.php') {
+ $message = "Lithium core could not be found. Check the value of LITHIUM_LIBRARY_PATH in ";
+ $message .= __FILE__ . ". It should point to the directory containing your ";
+ $message .= "/libraries directory.";
+ throw new ErrorException($message);
+}
+
+lithium\core\Libraries::add('lithium');
+
if ($app) {
- include $app . '/config/bootstrap.php';
-} else {
- define('LITHIUM_LIBRARY_PATH', $library);
- define('LITHIUM_APP_PATH', dirname($library) . '/app');
-
- if (!include LITHIUM_LIBRARY_PATH . '/lithium/core/Libraries.php') {
- $message = "Lithium core could not be found. Check the value of `LITHIUM_LIBRARY_PATH` ";
- $message .= "in `config/bootstrap.php`. It should point to the directory containing your ";
- $message .= "`/libraries` directory.";
- trigger_error($message, E_USER_ERROR);
- }
- Libraries::add('lithium');
+ lithium\core\Libraries::add(basename(LITHIUM_APP_PATH), array(
+ 'path' => LITHIUM_APP_PATH,
+ 'default' => true,
+ 'bootstrap' => !file_exists("{$app}/webroot/index.php")
+ ));
}
-exit(Dispatcher::run()->status);
+/**
+ * The following will dispatch the request and exit with the status code as
+ * provided by the `Response` object returned from `run()`.
+ *
+ * The following will instantiate a new `Request` object and pass it off to the
+ * `Dispatcher` class. By default, the `Request` will automatically aggregate
+ * all the server / environment settings, and request content (i.e. options and
+ * arguments passed to the command) information.
+ *
+ * The `Request` is then used by the `Dispatcher` (in conjunction with the
+ * `Router`) to determine the correct command to dispatch to. The response
+ * information is then encapsulated in a `Response` object, which is returned
+ * from the command to the `Dispatcher`.
+ *
+ * The `Response` object will contain information about the status code which
+ * is used as the exit code when ending the execution of this script and
+ * returned to the callee.
+ *
+ * @see lithium\console\Request
+ * @see lithium\console\Response
+ * @see lithium\console\Dispatcher
+ * @see lithium\console\Router
+ */
+exit(lithium\console\Dispatcher::run(new lithium\console\Request())->status);
?>
\ No newline at end of file
diff --git a/libraries/lithium/console/readme.wiki b/libraries/lithium/console/readme.wiki
index 13e1712..2d1d4aa 100644
--- a/libraries/lithium/console/readme.wiki
+++ b/libraries/lithium/console/readme.wiki
@@ -64,7 +64,8 @@ Creating your own commands is very easy.
**Fundamentals**
- All commands inherit from `lithium\console\Command`.
-- Commands are normally placed in your application or plugin's `exentesions/commands` directory.
+
+- Commands are normally placed in your application or plugin's `extensions/command` directory.
Here's an example command:
{{{<?php
@@ -115,13 +116,8 @@ class HelloWorld extends \lithium\console\Command {
public function run() {
$this->header('Welcome to the Hello World command!');
- if ($this->recipient) {
- $this->out('Hello, ' . $this->recipient . '!');
- } else {
- $this->out('Hello, World!');
- }
+ $this->out('Hello, ' . ($this->recipient ?: 'World') . '!');
}
-
}
?>}}}
diff --git a/libraries/lithium/core/Adaptable.php b/libraries/lithium/core/Adaptable.php
index 97ef5f9..21870a5 100644
--- a/libraries/lithium/core/Adaptable.php
+++ b/libraries/lithium/core/Adaptable.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\core;
-use \Exception;
-use \lithium\util\Collection;
-use \lithium\core\Environment;
-use \SplDoublyLinkedList;
+use lithium\util\Collection;
+use lithium\core\Environment;
+use SplDoublyLinkedList;
+use lithium\core\ConfigException;
/**
* The `Adaptable` static class is the base class from which all adapter implementations extend.
@@ -27,8 +27,6 @@ use \SplDoublyLinkedList;
* should be located.
*
* This static class should **never** be called explicitly.
- *
- * @todo Implement as abstract class with abstract method `adapter` when Inspector has been fixed.
*/
class Adaptable extends \lithium\core\StaticObject {
@@ -75,6 +73,7 @@ class Adaptable extends \lithium\core\StaticObject {
return static::_config($config);
}
$result = array();
+ static::$_configurations = array_filter(static::$_configurations);
foreach (array_keys(static::$_configurations) as $key) {
$result[$key] = static::_config($key);
@@ -102,41 +101,51 @@ class Adaptable extends \lithium\core\StaticObject {
$config = static::_config($name);
if ($config === null) {
- throw new Exception("Configuration $name has not been defined");
+ throw new ConfigException("Configuration `{$name}` has not been defined.");
}
- if (isset($config['adapter']) && is_object($config['adapter'])) {
- return $config['adapter'];
+ if (isset($config['object'])) {
+ return $config['object'];
}
$class = static::_class($config, static::$_adapters);
$settings = static::$_configurations[$name];
- $settings[0]['adapter'] = new $class($config);
+ $settings[0]['object'] = new $class($config);
static::$_configurations[$name] = $settings;
- return static::$_configurations[$name][0]['adapter'];
+ return static::$_configurations[$name][0]['object'];
}
/**
- * Obtain an SplStack of the strategies for the given `$name` configuration, using
- * the `$_strategies` path defined in Adaptable subclasses.
+ * Obtain an `SplStack` of the strategies for the given `$name` configuration, using
+ * the `$_strategies` path defined in `Adaptable` subclasses.
*
* @param string $name Class name of adapter to load.
- * return object SplStack of strategies, or null if none defined.
+ * @return object `SplStack` of strategies, or `null` if none are defined.
*/
public static function strategies($name) {
$config = static::_config($name);
if ($config === null) {
- throw new Exception("Configuration $name has not been defined");
+ throw new ConfigException("Configuration `{$name}` has not been defined.");
}
if (!isset($config['strategies'])) {
return null;
}
$stack = new SplDoublyLinkedList();
- foreach ($config['strategies'] as $strategy) {
- $class = static::_strategy($strategy, static::$_strategies);
- $stack->push(new $class($config));
+ foreach ($config['strategies'] as $key => $strategy) {
+ $arguments = array();
+
+ if (is_array($strategy)) {
+ $name = $key;
+ $class = static::_strategy($name, static::$_strategies);
+ $index = (isset($config['strategies'][$name])) ? $name : $class;
+ $arguments = $config['strategies'][$index];
+ } else {
+ $name = $strategy;
+ $class = static::_strategy($name, static::$_strategies);
+ }
+ $stack->push(new $class($arguments));
}
return $stack;
}
@@ -147,12 +156,14 @@ class Adaptable extends \lithium\core\StaticObject {
* @param string $method The strategy method to be applied.
* @param string $name The named configuration
* @param mixed $data The data to which the strategies will be applied.
- * @param null|string $mode If `$mode` is set to 'LIFO', the strategies are applied in reverse.
+ * @param array $options If `mode` is set to 'LIFO', the strategies are applied in reverse.
* order of their definition.
* @return mixed Result of application of strategies to data. If no strategies
* have been configured, this method will simply return the original data.
*/
- public static function applyStrategies($method, $name, $data, $mode = null) {
+ public static function applyStrategies($method, $name, $data, array $options = array()){
+ $options += array('mode' => null);
+
if (!$strategies = static::strategies($name)) {
return $data;
}
@@ -160,13 +171,14 @@ class Adaptable extends \lithium\core\StaticObject {
return $data;
}
- if ($mode === 'LIFO') {
+ if (isset($options['mode']) && ($options['mode'] === 'LIFO')) {
$strategies->setIteratorMode(SplDoublyLinkedList::IT_MODE_LIFO);
+ unset($options['mode']);
}
foreach ($strategies as $strategy) {
if (method_exists($strategy, $method)) {
- $data = $strategy::$method($data);
+ $data = $strategy->{$method}($data, $options);
}
}
return $data;
@@ -201,11 +213,11 @@ class Adaptable extends \lithium\core\StaticObject {
protected static function _class($config, $paths = array()) {
if (!$name = $config['adapter']) {
$self = get_called_class();
- throw new Exception("No adapter set for configuration in class {$self}");
+ throw new ConfigException("No adapter set for configuration in class `{$self}`.");
}
if (!$class = static::_locate($paths, $name)) {
$self = get_called_class();
- throw new Exception("Could not find adapter {$name} in class {$self}");
+ throw new ConfigException("Could not find adapter `{$name}` in class `{$self}`.");
}
return $class;
}
@@ -221,11 +233,11 @@ class Adaptable extends \lithium\core\StaticObject {
protected static function _strategy($name, $paths = array()) {
if (!$name) {
$self = get_called_class();
- throw new Exception("No strategy set for configuration in class {$self}");
+ throw new ConfigException("No strategy set for configuration in class `{$self}`.");
}
if (!$class = static::_locate($paths, $name)) {
$self = get_called_class();
- throw new Exception("Could not find strategy {$name} in class {$self}");
+ throw new ConfigException("Could not find strategy `{$name}` in class `{$self}`.");
}
return $class;
}
@@ -269,6 +281,10 @@ class Adaptable extends \lithium\core\StaticObject {
}
$env = Environment::get();
$config = isset($settings[$env]) ? $settings[$env] : $settings;
+
+ if (isset($settings[$env]) && isset($settings[true])) {
+ $config += $settings[true];
+ }
static::$_configurations[$name] += array(static::_initConfig($name, $config));
return static::$_configurations[$name][0];
}
diff --git a/libraries/lithium/core/ClassNotFoundException.php b/libraries/lithium/core/ClassNotFoundException.php
new file mode 100644
index 0000000..9ce69ca
--- /dev/null
+++ b/libraries/lithium/core/ClassNotFoundException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+/**
+ * A `ClassNotFoundException` may be thrown when a configured adapter or other service class defined
+ * in configuration can't be located.
+ */
+class ClassNotFoundException extends \RuntimeException {
+
+ protected $code = 500;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/ConfigException.php b/libraries/lithium/core/ConfigException.php
new file mode 100644
index 0000000..1021882
--- /dev/null
+++ b/libraries/lithium/core/ConfigException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+/**
+ * A `ConfigException` is thrown when a request is made to render content in a format not
+ * supported.
+ */
+class ConfigException extends \RuntimeException {
+
+ protected $code = 500;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/Environment.php b/libraries/lithium/core/Environment.php
index aa60a38..1894842 100644
--- a/libraries/lithium/core/Environment.php
+++ b/libraries/lithium/core/Environment.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\core;
-use \lithium\util\Set;
+use lithium\util\Set;
/**
* The `Environment` class allows you to manage multiple configurations for your application,
@@ -172,8 +172,13 @@ class Environment {
* environment's entire configuration as an array.
*/
public static function get($name = null) {
- if (empty($name)) {
- return static::$_current;
+ $cur = static::$_current;
+
+ if (!$name) {
+ return $cur;
+ }
+ if ($name === true) {
+ return isset(static::$_configurations[$cur]) ? static::$_configurations[$cur] : null;
}
if (isset(static::$_configurations[$name])) {
return static::$_configurations[$name];
@@ -181,6 +186,7 @@ class Environment {
if (!isset(static::$_configurations[static::$_current])) {
return null;
}
+
$config = static::$_configurations[static::$_current];
return isset($config[$name]) ? $config[$name] : null;
}
@@ -234,6 +240,7 @@ class Environment {
}
return;
}
+ $env = ($env === true) ? static::$_current : $env;
$base = isset(static::$_configurations[$env]) ? static::$_configurations[$env] : array();
return static::$_configurations[$env] = Set::merge($base, $config);
}
diff --git a/libraries/lithium/core/ErrorHandler.php b/libraries/lithium/core/ErrorHandler.php
new file mode 100644
index 0000000..774f6fb
--- /dev/null
+++ b/libraries/lithium/core/ErrorHandler.php
@@ -0,0 +1,346 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2009, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+use Exception;
+use ErrorException;
+use lithium\util\collection\Filters;
+
+/**
+ * The `ErrorHandler` class allows PHP errors and exceptions to be handled in a uniform way. Using
+ * the `ErrorHandler`'s configuration, it is possible to have very broad but very tight control
+ * over error handling in your application.
+ *
+ * {{{ embed:lithium\tests\cases\core\ErrorHandlerTest::testExceptionCatching(2-7) }}}
+ *
+ * Using a series of cascading rules and handlers, it is possible to capture and handle very
+ * specific errors and exceptions.
+ */
+class ErrorHandler extends \lithium\core\StaticObject {
+
+ /**
+ * Configuration parameters.
+ *
+ * @var array Config params
+ */
+ protected static $_config = array();
+
+ /**
+ * Error/exception handlers.
+ *
+ * @var array An array of closures that represent all invokable error/exception handlers.
+ */
+ protected static $_handlers = array();
+
+ /**
+ * Types of checks available for sorting & parsing errors/exceptions.
+ * Default checks are for `code`, `stack` and `message`.
+ *
+ * @var array Array of checks represented as closures, indexed by name.
+ */
+ protected static $_checks = array();
+
+ /**
+ * Currently registered exception handler.
+ *
+ * @var closure Closure representing exception handler.
+ */
+ protected static $_exceptionHandler = null;
+
+ /**
+ * State of error/exception handling.
+ *
+ * @var boolean True if custom error/exception handlers have been registered, false
+ * otherwise.
+ */
+ protected static $_isRunning = false;
+
+ protected static $_runOptions = array();
+
+ /**
+ * Setup basic error handling checks/types, as well as register the error and exception
+ * hanlders.
+ *
+ * Called on static class initialization (i.e. when first loaded).
+ *
+ * @return void
+ */
+ public static function __init() {
+ static::$_checks = array(
+ 'type' => function($config, $info) {
+ return (boolean) array_filter((array) $config['type'], function($type) use ($info) {
+ return $type == $info['type'] || is_subclass_of($info['type'], $type);
+ });
+ },
+ 'code' => function($config, $info) {
+ return ($config['code'] & $info['code']);
+ },
+ 'stack' => function($config, $info) {
+ return (boolean) array_intersect((array) $config['stack'], $info['stack']);
+ },
+ 'message' => function($config, $info) {
+ return preg_match($config['message'], $info['message']);
+ }
+ );
+ $self = get_called_class();
+
+ static::$_exceptionHandler = function($exception, $return = false) use ($self) {
+ $info = compact('exception') + array(
+ 'type' => get_class($exception),
+ 'stack' => $self::trace($exception->getTrace())
+ );
+ foreach (array('message', 'file', 'line', 'trace') as $key) {
+ $method = 'get' . ucfirst($key);
+ $info[$key] = $exception->{$method}();
+ }
+ return $return ? $info : $self::handle($info);
+ };
+ }
+
+ /**
+ * Getter & setter of error/exception handlers.
+ *
+ * @param array $handlers If set, the passed `$handlers` array will be merged with
+ * the already defined handlers in the `ErrorHandler` static class.
+ * @return array Current set of handlers.
+ */
+ public static function handlers($handlers = array()) {
+ return (static::$_handlers = $handlers + static::$_handlers);
+ }
+
+ /**
+ * Configure the `ErrorHandler`.
+ *
+ * @param array $config Configuration directives.
+ * @return Current configuration set.
+ */
+ public static function config($config = array()) {
+ return (static::$_config = array_merge($config, static::$_config));
+ }
+
+ public static function handler($name, $info) {
+ return static::$_handlers[$name]($info);
+ }
+
+ /**
+ * Register error and exception handlers.
+ *
+ * This method (`ErrorHandler::run()`) needs to be called as early as possible in the bootstrap
+ * cycle; immediately after `require`-ing `bootstrap/libraries.php` is your best bet.
+ *
+ * @param array $config The configuration with which to start the error handler. Available
+ * options include:
+ * - `'trapErrors'` _boolean_: Defaults to `false`. If set to `true`, PHP errors
+ * will be caught by `ErrorHandler` and handled in-place. Execution will resume
+ * in the same context in which the error occurred.
+ * - `'convertErrors'` _boolean_: Defaults to `true`, and specifies that all PHP
+ * errors should be converted to `ErrorException`s and thrown from the point
+ * where the error occurred. The exception will be caught at the first point in
+ * the stack trace inside a matching `try`/`catch` block, or that has a matching
+ * error handler applied using the `apply()` method.
+ * @return void
+ */
+ public static function run(array $config = array()) {
+ $defaults = array('trapErrors' => false, 'convertErrors' => true);
+
+ if (static::$_isRunning) {
+ return;
+ }
+ static::$_isRunning = true;
+ static::$_runOptions = $config + $defaults;
+ $self = get_called_class();
+
+ $trap = function($code, $message, $file, $line = 0, $context = null) use ($self) {
+ $trace = debug_backtrace();
+ $trace = array_slice($trace, 1, count($trace));
+ $self::handle(compact('type', 'code', 'message', 'file', 'line', 'trace', 'context'));
+ };
+
+ $convert = function($code, $message, $file, $line = 0, $context = null) use ($self) {
+ throw new ErrorException($message, 500, $code, $file, $line);
+ };
+
+ if (static::$_runOptions['trapErrors']) {
+ set_error_handler($trap);
+ } elseif (static::$_runOptions['convertErrors']) {
+ set_error_handler($convert);
+ }
+ set_exception_handler(static::$_exceptionHandler);
+ }
+
+ /**
+ * Returns the state of the `ErrorHandler`, indicating whether or not custom error/exception
+ * handers have been regsitered.
+ *
+ * @return void
+ */
+ public static function isRunning() {
+ return static::$_isRunning;
+ }
+
+ /**
+ * Unooks `ErrorHandler`'s exception and error handlers, and restores PHP's defaults. May have
+ * unexpected results if it is not matched with a prior call to `run()`, or if other error
+ * handlers are set after a call to `run()`.
+ *
+ * @return void
+ */
+ public static function stop() {
+ restore_error_handler();
+ restore_exception_handler();
+ static::$_isRunning = false;
+ }
+
+ /**
+ * Wipes out all configuration and resets the error handler to its initial state when loaded.
+ * Mainly used for testing.
+ *
+ * @return void
+ */
+ public static function reset() {
+ static::$_config = array();
+ static::$_checks = array();
+ static::$_handlers = array();
+ static::$_exceptionHandler = null;
+ static::__init();
+ }
+
+ /**
+ * Receives the handled errors and exceptions that have been caught, and processes them
+ * in a normalized manner.
+ *
+ * @param object|array $info
+ * @param array $scope
+ * @return boolean True if successfully handled, false otherwise.
+ */
+ public static function handle($info, $scope = array()) {
+ $checks = static::$_checks;
+ $rules = $scope ?: static::$_config;
+ $handler = static::$_exceptionHandler;
+ $info = is_object($info) ? $handler($info, true) : $info;
+
+ $defaults = array(
+ 'type' => null, 'code' => 0, 'message' => null, 'file' => null, 'line' => 0,
+ 'trace' => array(), 'context' => null, 'exception' => null
+ );
+ $info = (array) $info + $defaults;
+
+ $info['stack'] = static::trace($info['trace']);
+ $info['origin'] = static::_origin($info['trace']);
+
+ foreach ($rules as $config) {
+ foreach (array_keys($config) as $key) {
+ if ($key == 'conditions' || $key == 'scope' || $key == 'handler') {
+ continue;
+ }
+ if (!isset($info[$key]) || !isset($checks[$key])) {
+ continue 2;
+ }
+ if (($check = $checks[$key]) && !$check($config, $info)) {
+ continue 2;
+ }
+ }
+ if (!isset($config['handler'])) {
+ return false;
+ }
+ if ((isset($config['conditions']) && $call = $config['conditions']) && !$call($info)) {
+ return false;
+ }
+ if ((isset($config['scope'])) && static::handle($info, $config['scope']) !== false) {
+ return true;
+ }
+ $handler = $config['handler'];
+ return $handler($info) !== false;
+ }
+ return false;
+ }
+
+ /**
+ * Determine frame from the stack trace where the error/exception was first generated.
+ *
+ * @param array $stack Stack trace from error/exception that was produced.
+ * @return string Class where error/exception was generated.
+ */
+ protected static function _origin(array $stack) {
+ foreach ($stack as $frame) {
+ if (isset($frame['class'])) {
+ return trim($frame['class'], '\\');
+ }
+ }
+ }
+
+ public static function apply($object, array $conditions, $handler) {
+ $conditions = $conditions ?: array('type' => 'Exception');
+ list($class, $method) = is_string($object) ? explode('::', $object) : $object;
+ $wrap = static::$_exceptionHandler;
+ $_self = get_called_class();
+
+ $filter = function($self, $params, $chain) use ($_self, $conditions, $handler, $wrap) {
+ try {
+ return $chain->next($self, $params, $chain);
+ } catch (Exception $e) {
+ if (!$_self::matches($e, $conditions)) {
+ throw $e;
+ }
+ return $handler($wrap($e, true), $params);
+ }
+ };
+
+ if (is_string($class)) {
+ Filters::apply($class, $method, $filter);
+ } else {
+ $class->applyFilter($method, $filter);
+ }
+ }
+
+ public static function matches($info, $conditions) {
+ $checks = static::$_checks;
+ $handler = static::$_exceptionHandler;
+ $info = is_object($info) ? $handler($info, true) : $info;
+
+ foreach (array_keys($conditions) as $key) {
+ if ($key == 'conditions' || $key == 'scope' || $key == 'handler') {
+ continue;
+ }
+ if (!isset($info[$key]) || !isset($checks[$key])) {
+ return false;
+ }
+ if (($check = $checks[$key]) && !$check($conditions, $info)) {
+ return false;
+ }
+ }
+ if ((isset($config['conditions']) && $call = $config['conditions']) && !$call($info)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Trim down a typical stack trace to class & method calls.
+ *
+ * @param array $stack A `debug_backtrace()`-compatible stack trace output.
+ * @return array Returns a flat stack array containing class and method references.
+ */
+ public static function trace(array $stack) {
+ $result = array();
+
+ foreach ($stack as $frame) {
+ if (isset($frame['function'])) {
+ if (isset($frame['class'])) {
+ $result[] = trim($frame['class'], '\\') . '::' . $frame['function'];
+ } else {
+ $result[] = $frame['function'];
+ }
+ }
+ }
+ return $result;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/Libraries.php b/libraries/lithium/core/Libraries.php
index ebaa9c8..4680a1d 100644
--- a/libraries/lithium/core/Libraries.php
+++ b/libraries/lithium/core/Libraries.php
@@ -2,33 +2,35 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\core;
-use \Exception;
-use \lithium\util\String;
-use \InvalidArgumentException;
+use RuntimeException;
+use lithium\util\String;
+use lithium\core\ConfigException;
+use lithium\core\ClassNotFoundException;
/**
* Manages all aspects of class and file location, naming and mapping. Implements auto-loading for
* the Lithium core, as well as all applications, plugins and vendor libraries registered.
* Typically, libraries and plugins are registered in `app/config/bootstrap/libraries.php`.
*
- * By convention, vendor libraries are typically located in `app/libraries` or `/libraries`, and
- * plugins are located in `app/libraries/plugins` or `/libraries/plugins`. By default, `Libraries`
- * will use its own autoloader for all plugins and vendor libraries, but can be configured to use
- * others on a case-by-case basis.
+ * By convention, plugins and vendor libraries are typically located in `app/libraries` or
+ * `/libraries` (the former may override the latter). By default, `Libraries` will use its own
+ * autoloader for all plugins and vendor libraries, but can be configured to use others on a
+ * per-library basis.
*
- * `Libraries` also handles service location. Various 'types' of classes can be defined by name,
- * using _class patterns_, which define conventions for organizing classes, i.e. `'models'` is
- * `'{:library}\models\{:name}'`, which will find a model class in any registered app, plugin or
- * vendor library that follows that path (namespace) convention. You can find classes by name (see
- * `locate()` for more information on class-locating precedence), or find all models in all
- * registered libraries (apps / plugins / vendor libraries, etc). For more information on modifying
- * the default class organization, or defining your own class types, see the `paths()` method.
+ * `Libraries` also handles service location. Various _types_ of classes can be defined by name,
+ * using _class patterns_, which define conventions for organizing classes, i.e. `'models'`, which
+ * maps to `'{:library}\models\{:name}'`, which will find a model class in any registered app,
+ * plugin or vendor library that follows that path (namespace) convention. You can find classes by
+ * name (see the `locate()` method for more information on class-locating precedence), or find all
+ * models in all registered libraries (apps / plugins / vendor libraries, etc). For more information
+ * on modifying the default class organization, or defining your own class types, see the `paths()`
+ * method.
*
* #### Auto-loading classes
*
@@ -40,8 +42,8 @@ use \InvalidArgumentException;
* extensions, and are as follows:
*
* - Each library must exist in a top-level vendor namespace
- * - Each top-level vendor namespace must define a set of sub-packages, and should not contain
- * classes
+ * - Each top-level vendor namespace must define a set of sub-packages, and should not directly
+ * contain classes
* - Namespace names must be lowercased and under_scored
* - Class names must be CamelCased
*
@@ -55,6 +57,13 @@ use \InvalidArgumentException;
class Libraries {
/**
+ * Stores the closures that represent the method filters. They are indexed by method name.
+ *
+ * @var array
+ */
+ protected static $_methodFilters = array();
+
+ /**
* The list of class libraries registered with the class loader.
*
* @var array
@@ -80,10 +89,6 @@ class Libraries {
'{:library}\extensions\adapter\{:namespace}\{:class}\{:name}',
'{:library}\{:namespace}\{:class}\adapter\{:name}' => array('libraries' => 'lithium')
),
- 'strategy' => array(
- '{:library}\extensions\strategy\{:namespace}\{:class}\{:name}',
- '{:library}\{:namespace}\{:class}\strategy\{:name}' => array('libraries' => 'lithium')
- ),
'command' => array(
'{:library}\extensions\command\{:namespace}\{:class}\{:name}',
'{:library}\console\command\{:namespace}\{:class}\{:name}' => array(
@@ -95,7 +100,11 @@ class Libraries {
),
'data' => array(
'{:library}\extensions\data\{:namespace}\{:class}\{:name}',
- '{:library}\data\{:namespace}\{:class}\{:name}' => array('libraries' => 'lithium')
+ '{:library}\data\{:namespace}\{:class}\adapter\{:name}' => array(
+ 'libraries' => 'lithium'
+ ),
+ '{:library}\data\{:namespace}\{:class}\{:name}' => array('libraries' => 'lithium'),
+ '{:library}\data\{:class}\adapter\{:name}' => array('libraries' => 'lithium')
),
'helper' => array(
'{:library}\extensions\helper\{:name}',
@@ -103,18 +112,39 @@ class Libraries {
),
'libraries' => array(
'{:app}/libraries/{:name}',
- '{:root}/libraries/{:name}'
+ '{:root}/{:name}'
+ ),
+ 'models' => array(
+ '{:library}\models\{:name}'
+ ),
+ 'strategy' => array(
+ '{:library}\extensions\strategy\{:namespace}\{:class}\{:name}',
+ '{:library}\extensions\strategy\{:class}\{:name}',
+ '{:library}\{:namespace}\{:class}\strategy\{:name}' => array('libraries' => 'lithium')
+ ),
+ 'socket' => array(
+ '{:library}\extensions\net\socket\{:name}',
+ '{:library}\extensions\socket\{:name}',
+ '{:library}\net\socket\{:name}'
),
- 'models' => '{:library}\models\{:name}',
'test' => array(
'{:library}\extensions\test\{:namespace}\{:class}\{:name}',
- '{:library}\test\{:namespace}\{:class}\{:name}' => array('libraries' => 'lithium')
+ '{:library}\test\{:namespace}\{:class}\{:name}' => array('libraries' => 'lithium'),
),
'tests' => array(
- '{:library}\tests\{:namespace}\{:class}\{:name}Test'
+ '{:library}\tests\{:namespace}\{:class}\{:name}Test',
)
);
+ /**
+ * Stores the name of the default library. When adding a library configuration to the
+ * application, if the `'default'` option flag is set to `true`, the name of the library will
+ * be assigned. To retrieve the default library's configuration, use `Libraries::get(true)`.
+ *
+ * @see lithium\core\Libraries::add()
+ * @see lithium\core\Libraries::get()
+ * @var string
+ */
protected static $_default;
/**
@@ -186,31 +216,52 @@ class Libraries {
if (is_string($path)) {
return isset(static::$_paths[$path]) ? static::$_paths[$path] : null;
}
- static::$_paths = array_merge(static::$_paths, (array) $path);
+ static::$_paths = array_filter(array_merge(static::$_paths, (array) $path));
}
/**
- * Adds a class library from which files can be loaded
+ * Adds a class library from which files can be loaded.
+ *
+ * The `add()` method registers a named library configuration to your application, and is used
+ * to allow the framework to auto-load classes on an as-needed basis.
*
- * @param string $name Library name, i.e. `'app'`, `'lithium'`, `'pear'` or `'solar'`.
+ * ### Adding libraries to your application
+ *
+ * In Lithium, libraries represent the broadest unit of class organization in an application,
+ * and _everything_ is a library; this includes your application, and the Lithium framework
+ * itself. Libraries can also be other frameworks, like Solar, Zend Framework or PEAR, or
+ * Lithium plugins, which are simply libraries that follow the same organizational standards
+ * as Lithium applications.
+ *
+ * By convention, libraries are placed in the `libraries` directory inside your application, or
+ * the root `libraries` directory at the top level of the default distribution (i.e. the one
+ * that contains the `lithium` directory), however, you can change this on a case-by-case basis
+ * using the `'path'` key to specify an absolute path to the library's directory.
+ *
+ * @param string $name Library name, i.e. `'app'`, `'lithium'`, `'pear'` or `'aura'`.
* @param array $config Specifies where the library is in the filesystem, and how classes
* should be loaded from it. Allowed keys are:
- * - `'bootstrap'`: A file path (relative to `'path'`) to a bootstrap script that should
- * be run when the library is added.
- * - `'defer'`: If true, indicates that, when locating classes, this library should
- * defer to other libraries in order of preference.
- * - `'includePath'`: If `true`, appends the absolutely-resolved value of `'path'` to
- * the PHP include path.
+ * - `'bootstrap'` _mixed_: A file path (relative to `'path'`) to a bootstrap script that
+ * should be run when the library is added, or `true` to use the default bootstrap
+ * path, i.e. `config/bootstrap.php`.
+ * - `'defer'` _boolean_: If `true`, indicates that, when locating classes, this library
+ * should defer to other libraries in order of preference.
+ * - `'includePath'` _mixed_: If `true`, appends the absolutely-resolved value of
+ * `'path'` to the PHP include path. If a string, the value is appended to PHP's.
* - `'loader'`: An auto-loader method associated with the library, if any.
* - `'path'`: The directory containing the library.
- * - `'prefix'`: The class prefix this library uses, i.e. `'lithium\'`, `'Zend_'` or
- * `'Solar_'`.
- * - `'suffix'`: Gets appended to the end of the file name. For example, most libraries
- * end classes in `'.php'`, but some use `'.class.php'`, or `'.inc.php'`.
- * - `'transform'`: Defines a custom way to transform a class name into its
+ * - `'prefix'` _string_: The class prefix this library uses, i.e. `'lithium\'`,
+ * `'Zend_'` or `'Solar_'`. If the library has no global prefix, set to `false`.
+ * - `'suffix'` _string_: Gets appended to the end of the file name. For example, most
+ * libraries end classes in `'.php'`, but some use `'.class.php'`, or `'.inc.php'`.
+ * - `'transform'` _closure_: Defines a custom way to transform a class name into its
* corresponding file path. Accepts either an array of two strings which are
* interpreted as the pattern and replacement for a regex, or an anonymous function,
- * which receives the class name as a parameter, and returns a file path as output.
+ * which receives the class name and library configuration arrays as parameters, and
+ * returns the full physical file path as output.
+ * - `'resources'` _string_: If this is the default library, this maybe set to the
+ * absolute path to the write-enabled application resources directory, which is used
+ * for caching, log files, uploads, etc.
* @return array Returns the resulting set of options created for this library.
*/
public static function add($name, array $config = array()) {
@@ -223,7 +274,7 @@ class Libraries {
'transform' => null,
'bootstrap' => true,
'defer' => false,
- 'default' => false
+ 'default' => false,
);
if ($name === 'lithium') {
$defaults['defer'] = true;
@@ -235,20 +286,29 @@ class Libraries {
static::$_default = $name;
$defaults['path'] = LITHIUM_APP_PATH;
$defaults['bootstrap'] = false;
+ $defaults['resources'] = LITHIUM_APP_PATH . '/resources';
}
$config += $defaults;
if (!$config['path']) {
- $params = compact('name') + array(
- 'app' => LITHIUM_APP_PATH, 'root' => LITHIUM_LIBRARY_PATH
- );
- if (!$config['path'] = static::_locatePath('libraries', $params)) {
- throw new InvalidArgumentException("Library '{$name}' not found.");
+ if (!$config['path'] = static::_locatePath('libraries', compact('name'))) {
+ throw new ConfigException("Library `{$name}` not found.");
}
}
$config['path'] = str_replace('\\', '/', $config['path']);
- static::$_configurations[$name] = $config;
+ static::_configure(static::$_configurations[$name] = $config);
+ return $config;
+ }
+ /**
+ * Configures the application environment based on a library's settings, including appending to
+ * the include path, loading a bootstrap file, and registering a loader with SPL's autoloading
+ * system.
+ *
+ * @param array $config The new library's configuration array.
+ * @return void
+ */
+ protected static function _configure($config) {
if ($config['includePath']) {
$path = ($config['includePath'] === true) ? $config['path'] : $config['includePath'];
set_include_path(get_include_path() . PATH_SEPARATOR . $path);
@@ -263,23 +323,40 @@ class Libraries {
if (!empty($config['loader'])) {
spl_autoload_register($config['loader']);
}
- return $config;
}
/**
* Returns configuration for given name.
*
* @param string $name Registered library to retrieve configuration for.
- * @return array Retrieved configuration.
+ * @param string $key Optional key name. If `$name` is set and is the name of a valid library,
+ * returns the given named configuration key, i.e. `'path'`, `'webroot'` or
+ * `'resources'`.
+ * @return mixed A configuation array for one or more libraries, or a string value if `$key` is
+ * specified.
*/
- public static function get($name = null) {
- if (empty($name)) {
- return static::$_configurations;
+ public static function get($name = null, $key = null) {
+ $configs = static::$_configurations;
+
+ if (!$name) {
+ return $configs;
}
if ($name === true) {
$name = static::$_default;
}
- return isset(static::$_configurations[$name]) ? static::$_configurations[$name] : null;
+ if (is_array($name)) {
+ foreach ($name as $i => $key) {
+ unset($name[$i]);
+ $name[$key] = isset($configs[$key]) ? $configs[$key] : null;
+ }
+ return $name;
+ }
+ $config = isset($configs[$name]) ? $configs[$name] : null;
+
+ if (!$key) {
+ return $config;
+ }
+ return isset($config[$key]) ? $config[$key] : null;
}
/**
@@ -308,19 +385,19 @@ class Libraries {
* @return array
*/
public static function find($library, array $options = array()) {
- $format = function ($file, $config) {
+ $format = function($file, $config) {
$trim = array(strlen($config['path']) + 1, strlen($config['suffix']));
$rTrim = strpos($file, $config['suffix']) !== false ? -$trim[1] : 9999;
$file = preg_split('/[\/\\\\]/', substr($file, $trim[0], $rTrim));
return $config['prefix'] . join('\\', $file);
};
- $defaults = array(
- 'path' => '', 'recursive' => false,
+ $defaults = compact('format') + array(
+ 'path' => '',
+ 'recursive' => false,
'filter' => '/^(\w+)?(\\\\[a-z0-9_]+)+\\\\[A-Z][a-zA-Z0-9]+$/',
'exclude' => '',
- 'format' => $format,
- 'namespaces' => false
+ 'namespaces' => false,
);
$options += $defaults;
$libs = array();
@@ -336,19 +413,19 @@ class Libraries {
};
$options['filter'] = false;
}
-
if ($library === true) {
- foreach (array_keys(static::$_configurations) as $library) {
+ foreach (static::$_configurations as $library => $config) {
$libs = array_merge($libs, static::find($library, $options));
}
return $libs;
}
-
if (!isset(static::$_configurations[$library])) {
return null;
}
- $libs = static::_search(static::$_configurations[$library], $options);
+ $config = static::$_configurations[$library];
+ $options['path'] = "{$config['path']}{$options['path']}/*";
+ $libs = static::_search($config, $options);
return array_values(array_filter($libs));
}
@@ -359,7 +436,6 @@ class Libraries {
*
* @see lithium\core\Libraries::add()
* @see lithium\core\Libraries::path()
- *
* @param string $class The fully-namespaced (where applicable) name of the class to load.
* @param boolean $require Specifies whether the class must be loaded or considered an
* exception. Defaults to `false`.
@@ -373,7 +449,7 @@ class Libraries {
static::$_cachedPaths[$class] = $path;
method_exists($class, '__init') ? $class::__init() : null;
} elseif ($require) {
- throw new Exception("Failed to load {$class} from {$path}");
+ throw new RuntimeException("Failed to load class `{$class}` from path `{$path}`.");
}
}
@@ -402,30 +478,157 @@ class Libraries {
$params = $options + $config;
$suffix = $params['suffix'];
- if (strpos($class, $params['prefix']) !== 0) {
+ if ($params['prefix'] && strpos($class, $params['prefix']) !== 0) {
continue;
}
- if (!empty($params['transform'])) {
- if (is_callable($params['transform'])) {
- return $params['transform']($class, $params);
+ if ($transform = $params['transform']) {
+ if ($file = static::_transformPath($transform, $class, $params)) {
+ return $file;
}
- list($match, $replace) = $params['transform'];
- return preg_replace($match, $replace, $class);
+ continue;
}
$path = str_replace("\\", '/', substr($class, strlen($params['prefix'])));
$fullPath = "{$params['path']}/{$path}";
- if ($options['dirs']) {
- $list = glob(dirname($fullPath) . '/*');
- $list = array_map(function($i) { return str_replace('\\', '/', $i); }, $list);
+ if (!$options['dirs']) {
+ return static::$_cachedPaths[$class] = static::realPath($fullPath . $suffix);
+ }
+ $list = glob(dirname($fullPath) . '/*');
+ $list = array_map(function($i) { return str_replace('\\', '/', $i); }, $list);
- if (in_array($fullPath . $suffix, $list)) {
- return static::$_cachedPaths[$class] = $fullPath . $suffix;
- }
- return is_dir($fullPath) ? $fullPath : null;
+ if (in_array($fullPath . $suffix, $list)) {
+ return static::$_cachedPaths[$class] = static::realPath($fullPath . $suffix);
+ }
+ return is_dir($fullPath) ? static::realPath($fullPath) : null;
+ }
+ }
+
+ /**
+ * Wraps the PHP `realpath()` function to add support for finding paths to files inside Phar
+ * archives.
+ *
+ * @param string $path An unresolved path to a file inside a Phar archive which may or may not
+ * exist.
+ * @return string If `$path` is a valid path to a file inside a Phar archive, returns a string
+ * in the format `'phar://<path-to-phar>/<path-to-file>'`. Otherwise returns
+ * `null`.
+ */
+ public static function realPath($path) {
+ if (($absolutePath = realpath($path)) !== false) {
+ return $absolutePath;
+ }
+ if (!preg_match('%^phar://([^.]+\.phar(?:\.gz)?)(.+)%', $path, $pathComponents)) {
+ return;
+ }
+ list(, $relativePath, $pharPath) = $pathComponents;
+
+ $pharPath = implode('/', array_reduce(explode('/', $pharPath), function ($parts, $value) {
+ if ($value == '..') {
+ array_pop($parts);
+ } elseif ($value != '.') {
+ $parts[] = $value;
+ }
+ return $parts;
+ }));
+
+ if (($resolvedPath = realpath($relativePath)) !== false) {
+ if (file_exists($absolutePath = "phar://{$resolvedPath}{$pharPath}")) {
+ return $absolutePath;
+ }
+ }
+ }
+
+ /**
+ * Handles the conversion of a class name to a file name using a custom transformation typically
+ * defined in the `'transform'` key of a configuration defined through `Libraries::add()`.
+ *
+ * The transformation can either be a closure which receives two parameters (the class name
+ * as a string, and the library configuration as an array), or an array with two values (one
+ * being the pattern to match, the other being the replacement).
+ *
+ * @see lithium\core\Libraries::add()
+ * @see lithium\core\Libraries::path()
+ * @param mixed $transform Either a closure or an array containing a regular expression match
+ * and replacement. If the closure returns an empty value, or the regular
+ * expression fails to match, will return `null`.
+ * @param string $class The class name which is attempting to be mapped to a file.
+ * @param array $options The configuration of the library as passed to `Libraries::add()`, along
+ * with any options specified in the call to `Libraries::path()`.
+ * @return string Returns transformed path of a class to a file, or `null` if the transformation
+ * did not match.
+ */
+ protected static function _transformPath($transform, $class, array $options = array()) {
+ if ((is_callable($transform)) && $file = $transform($class, $options)) {
+ return $file;
+ }
+ if (is_array($transform)) {
+ list($match, $replace) = $transform;
+ return preg_replace($match, $replace, $class) ?: null;
+ }
+ }
+
+ /**
+ * Uses service location (i.e. `Libraries::locate()`) to look up a named class of a particular
+ * type, and creates an instance of it, and passes an array of parameters to the constructor.
+ *
+ * If the given class can't be found, an exception is thrown.
+ *
+ * @param string $type The type of class as defined by `Libraries::$_paths`.
+ * @param string $name The un-namespaced name of the class to instantiate.
+ * @param array $options An array of constructor parameters to pass to the class.
+ * @return object If the class is found, returns an instance of it, otherwise throws an
+ * exception.
+ * @throws lithium\core\ClassNotFoundException Throws an exception if the class can't be found.
+ * @filter
+ */
+ public static function instance($type, $name, array $options = array()) {
+ $params = compact('type', 'name', 'options');
+ $_paths =& static::$_paths;
+
+ $implementation = function($self, $params) use (&$_paths) {
+ $name = $params['name'];
+ $type = $params['type'];
+
+ if (!$name && !$type) {
+ $message = "Invalid class lookup: `\$name` and `\$type` are empty.";
+ throw new ClassNotFoundException($message);
+ }
+ if (!is_string($type) && $type !== null && !isset($_paths[$type])) {
+ throw new ClassNotFoundException("Invalid class type `{$type}`.");
}
- return static::$_cachedPaths[$class] = $fullPath . $suffix;
+ if (!$class = $self::locate($type, $name)) {
+ throw new ClassNotFoundException("Class `{$name}` of type `{$type}` not found.");
+ }
+ if (is_object($class)) {
+ return $class;
+ }
+ if (!(is_string($class) && class_exists($class))) {
+ throw new ClassNotFoundException("Class `{$name}` of type `{$type}` not defined.");
+ }
+ return new $class($params['options']);
+ };
+ if (!isset(static::$_methodFilters[__FUNCTION__])) {
+ return $implementation(get_called_class(), $params);
}
+ $class = get_called_class();
+ $method = __FUNCTION__;
+ $data = array_merge(static::$_methodFilters[__FUNCTION__], array($implementation));
+ return Filters::run($class, $params, compact('data', 'class', 'method'));
+ }
+
+ /**
+ * Apply a closure to a method in `Libraries`.
+ *
+ * @see lithium\util\collection\Filters
+ * @param string $method The name of the method to apply the closure to.
+ * @param closure $filter The closure that is used to filter the method.
+ * @return void
+ */
+ public static function applyFilter($method, $filter = null) {
+ if (!isset(static::$_methodFilters[$method])) {
+ static::$_methodFilters[$method] = array();
+ }
+ static::$_methodFilters[$method][] = $filter;
}
/**
@@ -483,42 +686,40 @@ class Libraries {
* found which match `$type`.
*/
public static function locate($type, $name = null, array $options = array()) {
- $defaults = array('type' => 'class');
- $options += $defaults;
-
if (is_object($name) || strpos($name, '\\') !== false) {
return $name;
}
- $ident = $name ? $type . '.' . $name : $type;
+ $ident = $name ? ($type . '.' . $name) : ($type . '.*');
+ $ident .= $options ? '.' . md5(serialize($options)) : null;
if (isset(static::$_cachedPaths[$ident])) {
return static::$_cachedPaths[$ident];
}
$params = static::_params($type, $name);
- extract($params);
- if (!isset(static::$_paths[$type])) {
+ $defaults = array(
+ 'type' => 'class',
+ 'library' => $params['library'] !== '*' ? $params['library'] : null
+ );
+ $options += $defaults;
+ unset($params['library']);
+ $paths = static::paths($params['type']);
+
+ if (!isset($paths)) {
return null;
}
- if (!$name) {
- return static::_locateAll($params, $options);
+ if ($params['name'] === '*') {
+ $result = static::_locateAll($params, $options);
+ return (static::$_cachedPaths[$ident] = $result);
}
- $paths = (array) static::$_paths[$type];
-
- if (strpos($name, '.')) {
- list($params['library'], $params['name']) = explode('.', $name);
- $params['library'][0] = strtolower($params['library'][0]);
-
- $result = static::_locateDeferred(null, $paths, $params, $options + array(
- 'library' => $params['library']
- ));
+ if ($options['library']) {
+ $result = static::_locateDeferred(null, $paths, $params, $options);
return static::$_cachedPaths[$ident] = $result;
}
- if ($result = static::_locateDeferred(false, $paths, $params, $options)) {
- return (static::$_cachedPaths[$ident] = $result);
- }
- if ($result = static::_locateDeferred(true, $paths, $params, $options)) {
- return (static::$_cachedPaths[$ident] = $result);
+ foreach (array(false, true) as $defer) {
+ if ($result = static::_locateDeferred($defer, $paths, $params, $options)) {
+ return (static::$_cachedPaths[$ident] = $result);
+ }
}
}
@@ -560,37 +761,21 @@ class Libraries {
* class in any path matching any of the parameters is located.
*/
protected static function _locateDeferred($defer, $paths, $params, array $options = array()) {
+ $libraries = static::$_configurations;
+
if (isset($options['library'])) {
- $libraries = (array) $options['library'];
- $libraries = array_intersect_key(
- static::$_configurations,
- array_combine($libraries, array_fill(0, count($libraries), null))
- );
- } else {
- $libraries = static::$_configurations;
+ $libraries = static::get((array) $options['library']);
}
-
foreach ($libraries as $library => $config) {
if ($config['defer'] !== $defer && $defer !== null) {
continue;
}
- foreach ($paths as $pathTemplate => $pathOptions) {
- if (is_int($pathTemplate)) {
- $pathTemplate = $pathOptions;
- $pathOptions = array();
- }
- $opts = $options + $pathOptions;
-
- if (isset($opts['libraries']) && !in_array($library, (array) $opts['libraries'])) {
- unset($opts['libraries']);
- continue;
- }
-
+ foreach (static::_searchPaths($paths, $library, $params) as $tpl) {
$params['library'] = $library;
- $class = str_replace('\\*', '', String::insert($pathTemplate, $params));
+ $class = str_replace('\\*', '', String::insert($tpl, $params));
- if (file_exists($file = Libraries::path($class, $opts))) {
+ if (file_exists($file = Libraries::path($class, $options))) {
return ($options['type'] === 'file') ? $file : $class;
}
}
@@ -598,62 +783,77 @@ class Libraries {
}
/**
- * Locates all possible classes for given params
+ * Returns the list of valid search path templates for the given service location lookup.
*
- * @param string $params
- * @param string $options
- * @return void
+ * @see lithium\core\Libraries::$_paths
+ * @see lithium\core\Libraries::_search()
+ * @param array $paths The list of all possible path templates from `Libraries::$_paths`.
+ * @param string $library The name of the library being searched.
+ * @param array $params The parameters used in the service location lookup.
+ * @return array Returns an array of valid path template strings.
*/
- protected static function _locateAll(array $params, array $options = array()) {
- $defaults = array(
- 'libraries' => null, 'recursive' => true, 'namespaces' => false,
- 'filter' => false, 'exclude' => false,
- 'format' => function ($file, $config) {
- $trim = array(strlen($config['path']) + 1, strlen($config['suffix']));
- $file = substr($file, $trim[0], -$trim[1]);
- return $config['prefix'] . str_replace('/', '\\', $file);
+ protected static function _searchPaths($paths, $library, $params) {
+ $result = array();
+ $params = array('library' => null, 'type' => null) + $params;
+
+ foreach ($paths as $tpl => $opts) {
+ if (is_int($tpl)) {
+ $tpl = $opts;
+ $opts = array();
}
- );
+ if (isset($opts['libraries']) && !in_array($library, (array) $opts['libraries'])) {
+ continue;
+ }
+ $result[] = $tpl;
+ }
+ return $result;
+ }
+
+ /**
+ * Locates all possible classes for given set of parameters.
+ *
+ * @param array $params
+ * @param array $options
+ * @return array
+ */
+ protected static function _locateAll(array $params, array $options = array()) {
+ $defaults = array('libraries' => null, 'recursive' => true, 'namespaces' => false);
$options += $defaults;
+
$paths = (array) static::$_paths[$params['type']];
- $libraries = $options['libraries'] ?: array_keys(static::$_configurations);
+ $libraries = $options['library'] ? $options['library'] : $options['libraries'];
+ $libraries = static::get((array) $libraries);
+ $flags = array('escape' => '/');
$classes = array();
- foreach ($libraries as $library) {
- $config = static::$_configurations[$library];
-
- foreach ($paths as $template => $tplOpts) {
- if (is_int($template)) {
- $template = $tplOpts;
- $tplOpts = array();
- }
- $opts = $options + $tplOpts;
-
- if (isset($opts['libraries']) && !in_array($library, (array) $opts['libraries'])) {
- unset($opts['libraries']);
- continue;
- }
- $path = String::insert($template, $params, array('escape' => '/'));
- $parts = explode('\\', $path);
- $name = end($parts);
-
- $pattern = '/(\/\*)|(\/(?:[A-Z][a-z0-9_]*))|({:\w+})/';
- $opts['path'] = preg_replace($pattern, '', str_replace('\\', '/', $path));
+ foreach ($libraries as $library => $config) {
+ $params['library'] = $config['path'];
- if (is_dir(str_replace('//', '/', "{$config['path']}/{$opts['path']}"))) {
- $classes = array_merge($classes, static::_search($config, $opts, $name));
- }
+ foreach (static::_searchPaths($paths, $library, $params) as $tpl) {
+ $options['path'] = str_replace('\\', '/', String::insert($tpl, $params, $flags));
+ $options['path'] = str_replace('*/', '', $options['path']);
+ $classes = array_merge($classes, static::_search($config, $options));
}
}
- return $classes;
+ return array_unique($classes);
}
+ /**
+ * Helper function for returning known paths given a certain type.
+ *
+ * @see lithium\core\Libraries::$_paths
+ * @param string $type Path type (specified in `Libraries::$_paths`).
+ * @param string $params Path parameters.
+ * @return string Valid path name.
+ */
protected static function _locatePath($type, $params) {
if (!isset(static::$_paths[$type])) {
return;
}
+ $params += array('app' => LITHIUM_APP_PATH, 'root' => LITHIUM_LIBRARY_PATH);
+
foreach (static::$_paths[$type] as $path) {
- if (is_dir($path = String::insert($path, $params))) {
+ if (is_dir($path = str_replace('\\', '/', String::insert($path, $params)))) {
return $path;
}
}
@@ -668,38 +868,64 @@ class Libraries {
* @return array
*/
protected static function _search($config, $options, $name = null) {
- $path = rtrim($config['path'] . $options['path'], '/');
+ $defaults = array(
+ 'path' => null,
+ 'suffix' => null,
+ 'namespaces' => false,
+ 'recursive' => false,
+ 'preFilter' => '/[A-Z][A-Za-z0-9]+\./',
+ 'filter' => false,
+ 'exclude' => false,
+ 'format' => function ($file, $config) {
+ $trim = array(strlen($config['path']) + 1, strlen($config['suffix']));
+ $file = substr($file, $trim[0], -$trim[1]);
+ return $config['prefix'] . str_replace('/', '\\', $file);
+ }
+ );
+ $options += $defaults;
+ $path = $options['path'];
+ $suffix = $options['namespaces'] ? '' : $config['suffix'];
+ $suffix = ($options['suffix'] === null) ? $suffix : $options['suffix'];
- $search = function($path) use ($config, $options) {
- $suffix = $options['namespaces'] ? '' : $config['suffix'];
- $suffix = isset($options['suffix']) ? $options['suffix'] : $suffix;
- return (array) glob($path . '/*' . $suffix);
- };
- $libs = $search($path);
+ $dFlags = GLOB_ONLYDIR;
+ $libs = (array) glob($path . $suffix, $options['namespaces'] ? $dFlags : 0);
- if ($options['namespaces'] === true) {
- $filter = '/^.+\/[A-Za-z0-9_]+$|^.*' . preg_quote($config['suffix'], '/') . '/';
- $libs = preg_grep($filter, $libs);
- }
if ($options['recursive']) {
- $dirs = $queue = array_diff((array) glob($path . '/*', GLOB_ONLYDIR), $libs);
- while ($queue) {
- $dir = array_pop($queue);
+ list($current, $match) = explode('/*', $path, 2);
+ $dirs = $queue = array_diff((array) glob($current . '/*', $dFlags), $libs);
+ $match = str_replace('##', '.+', preg_quote(str_replace('*', '##', $match), '/'));
+ $match = '/' . $match . preg_quote($suffix, '/') . '$/';
- if (!is_dir($dir)) {
+ while ($queue) {
+ if (!is_dir($dir = array_pop($queue))) {
continue;
}
- $libs = array_merge($libs, $search($dir));
- $queue = array_merge(
- $queue, array_diff((array) glob($dir . '/*', GLOB_ONLYDIR), $libs)
- );
+ $libs = array_merge($libs, (array) glob("{$dir}/*{$suffix}"));
+ $queue = array_merge($queue, array_diff((array) glob("{$dir}/*", $dFlags), $libs));
}
+ $libs = preg_grep($match, $libs);
+ }
+ if ($suffix) {
+ $libs = $options['preFilter'] ? preg_grep($options['preFilter'], $libs) : $libs;
}
+ return static::_filter($libs, (array) $config, $options + compact('name'));
+ }
+
+ /**
+ * Filters a list of library search results by the given set of options.
+ *
+ * @param array $libs List of found libraries.
+ * @param array $config The configuration of the library currently being searched within.
+ * @param array $options The options used to filter/format `$libs`.
+ * @return array Returns a copy of `$libs`, filtered and transformed based on the configuration
+ * provided in `$options`.
+ */
+ protected static function _filter($libs, array $config, array $options = array()) {
if (is_callable($options['format'])) {
foreach ($libs as $i => $file) {
$libs[$i] = $options['format']($file, $config);
}
- $libs = $name ? preg_grep("/{$name}$/", $libs) : $libs;
+ $libs = $options['name'] ? preg_grep("/{$options['name']}$/", $libs) : $libs;
}
if ($exclude = $options['exclude']) {
if (is_string($exclude)) {
@@ -722,12 +948,14 @@ class Libraries {
* Get params from type.
*
* @param string $type
- * @param string $name default: null
+ * @param string $name default: '*'
* @return array type, namespace, class, name
*/
- protected static function _params($type, $name = null) {
- $namespace = $class = '*';
- if (strpos($type, '.')) {
+ protected static function _params($type, $name = "*") {
+ $name = $name ?: "*";
+ $library = $namespace = $class = '*';
+
+ if (strpos($type, '.') !== false) {
$parts = explode('.', $type);
$type = array_shift($parts);
@@ -744,7 +972,13 @@ class Libraries {
break;
}
}
- return compact('type', 'namespace', 'class', 'name');
+ if (strpos($name, '.') !== false) {
+ $parts = explode('.', $name);
+ $library = array_shift($parts);
+ $name = array_pop($parts);
+ $namespace = $parts ? join('\\', $parts) : "*";
+ }
+ return compact('library', 'namespace', 'type', 'class', 'name');
}
}
diff --git a/libraries/lithium/core/NetworkException.php b/libraries/lithium/core/NetworkException.php
new file mode 100644
index 0000000..683dcbb
--- /dev/null
+++ b/libraries/lithium/core/NetworkException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+/**
+ * A `NetworkException` may be thrown whenever an unsuccessful attempt is made to connect to a
+ * remote service over the network. This may be a web service, a database, or another network
+ * resource.
+ */
+class NetworkException extends \RuntimeException {
+
+ protected $code = 503;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/Object.php b/libraries/lithium/core/Object.php
index e452bb2..54239b3 100644
--- a/libraries/lithium/core/Object.php
+++ b/libraries/lithium/core/Object.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\core;
-use \lithium\util\collection\Filters;
+use lithium\core\Libraries;
+use lithium\util\collection\Filters;
/**
* Base class in Lithium's hierarchy, from which all concrete classes inherit. This class defines
@@ -64,11 +65,20 @@ class Object {
*/
protected $_methodFilters = array();
+ /**
+ * Parents of the current class.
+ *
+ * @see lithium\core\Object::_parents()
+ * @var array
+ */
protected static $_parents = array();
/**
- * Initializes properties, unless supplied configuration options change the default behaviour.
+ * Initializes class configuration (`$_config`), and assigns object properties using the
+ * `_init()` method, unless otherwise specified by configuration. See below for details.
*
+ * @see lithium\core\Object::$_config
+ * @see lithium\core\Object::_init()
* @param array $config The configuration options which will be assigned to the `$_config`
* property. This method accepts one configuration option:
* - `'init'` _boolean_: Controls constructor behavior for calling the `_init()`
@@ -127,19 +137,19 @@ class Object {
/**
* Apply a closure to a method of the current object instance.
*
- * @param mixed $method The name of the method to apply the closure to. Can either be a single
- * method name as a string, or an array of method names.
- * @param closure $closure The closure that is used to filter the method(s).
- * @return void
* @see lithium\core\Object::_filter()
* @see lithium\util\collection\Filters
+ * @param mixed $method The name of the method to apply the closure to. Can either be a single
+ * method name as a string, or an array of method names.
+ * @param closure $filter The closure that is used to filter the method(s).
+ * @return void
*/
- public function applyFilter($method, $closure = null) {
+ public function applyFilter($method, $filter = null) {
foreach ((array) $method as $m) {
if (!isset($this->_methodFilters[$m])) {
$this->_methodFilters[$m] = array();
}
- $this->_methodFilters[$m][] = $closure;
+ $this->_methodFilters[$m][] = $filter;
}
}
@@ -191,6 +201,22 @@ class Object {
}
/**
+ * Returns an instance of a class with given `config`. The `name` could be a key from the
+ * `classes` array, a fully-namespaced class name, or an object. Typically this method is used
+ * in `_init` to create the dependencies used in the current class.
+ *
+ * @param string|object $name A `classes` key or fully-namespaced class name.
+ * @param array $options The configuration passed to the constructor.
+ * @return object
+ */
+ protected function _instance($name, array $options = array()) {
+ if (is_string($name) && isset($this->_classes[$name])) {
+ $name = $this->_classes[$name];
+ }
+ return Libraries::instance(null, $name, $options);
+ }
+
+ /**
* Executes a set of filters against a method by taking a method's main implementation as a
* callback, and iteratively wrapping the filters around it. This, along with the `Filters`
* class, is the core of Lithium's filters system. This system allows you to "reach into" an
@@ -212,12 +238,12 @@ class Object {
list($class, $method) = explode('::', $method);
if (empty($this->_methodFilters[$method]) && empty($filters)) {
- return $callback->__invoke($this, $params, null);
+ return $callback($this, $params, null);
}
$f = isset($this->_methodFilters[$method]) ? $this->_methodFilters[$method] : array();
- $items = array_merge($f, $filters, array($callback));
- return Filters::run($this, $params, compact('items', 'class', 'method'));
+ $data = array_merge($f, $filters, array($callback));
+ return Filters::run($this, $params, compact('data', 'class', 'method'));
}
/**
@@ -235,12 +261,13 @@ class Object {
}
/**
- * Exit immediately. Primarily used for overrides during testing.
+ * Exit immediately. Primarily used for overrides during testing.
*
+ * @param integer|string $status integer range 0 to 254, string printed on exit
* @return void
*/
- protected function _stop() {
- exit();
+ protected function _stop($status = 0) {
+ exit($status);
}
}
diff --git a/libraries/lithium/core/StaticObject.php b/libraries/lithium/core/StaticObject.php
index 3874391..9f30ee0 100644
--- a/libraries/lithium/core/StaticObject.php
+++ b/libraries/lithium/core/StaticObject.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\core;
-use \lithium\util\collection\Filters;
+use lithium\core\Libraries;
+use lithium\util\collection\Filters;
/**
* Provides a base class for all static classes in the Lithium framework. Similar to its
@@ -27,22 +28,29 @@ class StaticObject {
protected static $_methodFilters = array();
/**
+ * Keeps a cached list of each class' inheritance tree.
+ *
+ * @var array
+ */
+ protected static $_parents = array();
+
+ /**
* Apply a closure to a method of the current static object.
*
* @see lithium\core\StaticObject::_filter()
* @see lithium\util\collection\Filters
* @param mixed $method The name of the method to apply the closure to. Can either be a single
* method name as a string, or an array of method names.
- * @param closure $closure The closure that is used to filter the method.
+ * @param closure $filter The closure that is used to filter the method.
* @return void
*/
- public static function applyFilter($method, $closure = null) {
+ public static function applyFilter($method, $filter = null) {
$class = get_called_class();
foreach ((array) $method as $m) {
if (!isset(static::$_methodFilters[$class][$m])) {
static::$_methodFilters[$class][$m] = array();
}
- static::$_methodFilters[$class][$m][] = $closure;
+ static::$_methodFilters[$class][$m][] = $filter;
}
}
@@ -75,6 +83,22 @@ class StaticObject {
}
/**
+ * Returns an instance of a class with given `config`. The `name` could be a key from the
+ * `classes` array, a fully namespaced class name, or an object. Typically this method is used
+ * in `_init` to create the dependencies used in the current class.
+ *
+ * @param string|object $name A `classes` key or fully-namespaced class name.
+ * @param array $options The configuration passed to the constructor.
+ * @return object
+ */
+ protected static function _instance($name, array $options = array()) {
+ if (is_string($name) && isset(static::$_classes[$name])) {
+ $name = static::$_classes[$name];
+ }
+ return Libraries::instance(null, $name, $options);
+ }
+
+ /**
* Executes a set of filters against a method by taking a method's main implementation as a
* callback, and iteratively wrapping the filters around it.
*
@@ -88,31 +112,42 @@ class StaticObject {
* @return mixed
*/
protected static function _filter($method, $params, $callback, $filters = array()) {
- if (!strpos($method, '::')) {
- $class = get_called_class();
- } else {
- list($class, $method) = explode('::', $method);
- }
+ $class = get_called_class();
+ $hasNoFilters = empty(static::$_methodFilters[$class][$method]);
- if (empty(static::$_methodFilters[$class][$method]) && empty($filters)) {
- return $callback->__invoke($class, $params, null);
+ if ($hasNoFilters && !$filters && !Filters::hasApplied($class, $method)) {
+ return $callback($class, $params, null);
}
-
if (!isset(static::$_methodFilters[$class][$method])) {
static::$_methodFilters += array($class => array());
static::$_methodFilters[$class][$method] = array();
}
- $items = array_merge(static::$_methodFilters[$class][$method], $filters, array($callback));
- return Filters::run($class, $params, compact('items', 'class', 'method'));
+ $data = array_merge(static::$_methodFilters[$class][$method], $filters, array($callback));
+ return Filters::run($class, $params, compact('data', 'class', 'method'));
+ }
+
+ /**
+ * Gets and caches an array of the parent methods of a class.
+ *
+ * @return array Returns an array of parent classes for the current class.
+ */
+ protected static function _parents() {
+ $class = get_called_class();
+
+ if (!isset(self::$_parents[$class])) {
+ self::$_parents[$class] = class_parents($class);
+ }
+ return self::$_parents[$class];
}
/**
* Exit immediately. Primarily used for overrides during testing.
*
+ * @param integer|string $status integer range 0 to 254, string printed on exit
* @return void
*/
- protected static function _stop() {
- exit();
+ protected static function _stop($status = 0) {
+ exit($status);
}
}
diff --git a/libraries/lithium/data/Collection.php b/libraries/lithium/data/Collection.php
index 067b7a7..bc79012 100644
--- a/libraries/lithium/data/Collection.php
+++ b/libraries/lithium/data/Collection.php
@@ -2,42 +2,58 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data;
+use RuntimeException;
+
+/**
+ * The `Collection` class extends the generic `lithium\util\Collection` class to provide
+ * context-specific features for working with sets of data persisted by a backend data store. This
+ * is a general abstraction that operates on abitrary sets of data from either relational or
+ * non-relational data stores.
+ */
abstract class Collection extends \lithium\util\Collection {
/**
- * The fully-namespaced class name of the model object to which this record set is bound. This
- * is usually the model that executed the query which created this object.
+ * A reference to this object's parent `Document` object.
*
+ * @var object
+ */
+ protected $_parent = null;
+
+ /**
+ * If this `Collection` instance has a parent document (see `$_parent`), this value indicates
+ * the key name of the parent document that contains it.
+ *
+ * @see lithium\data\Collection::$_parent
* @var string
*/
- protected $_model = null;
+ protected $_pathKey = null;
/**
- * A reference to the object that originated this record set; usually an instance of
- * `lithium\data\Source` or `lithium\data\source\Database`. Used to load column definitions and
- * lazy-load records.
+ * The fully-namespaced class name of the model object to which this entity set is bound. This
+ * is usually the model that executed the query which created this object.
*
- * @var object
+ * @var string
*/
- protected $_handle = null;
+ protected $_model = null;
/**
- * A reference to the query object that originated this record set; usually an instance of
+ * A reference to the query object that originated this entity set; usually an instance of
* `lithium\data\model\Query`.
*
+ * @see lithium\data\model\Query
* @var object
*/
protected $_query = null;
/**
- * A pointer or resource that is used to load records from the object (`$_handle`) that
- * originated this record set.
+ * A pointer or resource that is used to load entities from the backend data source that
+ * originated this collection.
*
* @var resource
*/
@@ -53,49 +69,136 @@ abstract class Collection extends \lithium\util\Collection {
protected $_valid = true;
/**
- * By default, query results are not fetched until the record set is iterated. Set to true when
- * the record set has begun iterating and fetching records.
+ * Contains an array of backend-specific statistics generated by the query that produced this
+ * `Collection` object. These stats are accessible via the `stats()` method.
+ *
+ * @see lithium\data\Collection::stats()
+ * @var array
+ */
+ protected $_stats = array();
+
+ /**
+ * By default, query results are not fetched until the collection is iterated. Set to `true`
+ * when the collection has begun iterating and fetching entities.
*
+ * @see lithium\data\Collection::rewind()
+ * @see lithium\data\Collection::_populate()
* @var boolean
- * @see lithium\data\model\DataSet::rewind()
- * @see lithium\data\model\DataSet::_populate()
*/
protected $_hasInitialized = false;
+ protected $_schema = array();
+
/**
* Holds an array of values that should be processed on initialization.
*
* @var array
*/
protected $_autoConfig = array(
- 'items', 'classes' => 'merge', 'handle', 'model', 'result', 'query'
+ 'data', 'model', 'result', 'query', 'parent', 'stats', 'pathKey'
);
+ /**
+ * Class constructor
+ *
+ * @param array $config
+ * @return void
+ */
+ public function __construct(array $config = array()) {
+ $defaults = array('data' => array(), 'model' => null);
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+
+ foreach (array('data', 'classes', 'model', 'result', 'query') as $key) {
+ unset($this->_config[$key]);
+ }
+ if ($model = $this->_model) {
+ $options = array(
+ 'pathKey' => $this->_pathKey,
+ 'schema' => $model::schema(),
+ 'exists' => isset($this->_config['exists']) ? $this->_config['exists'] : null
+ );
+ $this->_data = $model::connection()->cast($this, $this->_data, $options);
+ }
+ }
+
+ /**
+ * Configures protected properties of a `Collection` so that it is parented to `$parent`.
+ *
+ * @param object $parent
+ * @param array $config
+ * @return void
+ */
+ public function assignTo($parent, array $config = array()) {
+ foreach ($config as $key => $val) {
+ $this->{'_' . $key} = $val;
+ }
+ $this->_parent =& $parent;
+ }
+
+ /**
+ * Returns the model which this particular collection is based off of.
+ *
+ * @return string The fully qualified model class name.
+ */
+ public function model() {
+ return $this->_model;
+ }
+
+ public function schema($field = null) {
+ $schema = array();
+
+ switch (true) {
+ case ($this->_schema):
+ $schema = $this->_schema;
+ break;
+ case ($model = $this->_model):
+ $schema = $model::schema();
+ break;
+ }
+ if ($field) {
+ return isset($self->_schema[$field]) ? $self->_schema[$field] : null;
+ }
+ return $schema;
+ }
+
+ /**
+ * Returns a boolean indicating whether an offset exists for the
+ * current `Collection`.
+ *
+ * @param string $offset String or integer indicating the offset or
+ * index of an entity in the set.
+ * @return boolean Result.
+ */
public function offsetExists($offset) {
return ($this->offsetGet($offset) !== null);
}
/**
- * Reset the set's iterator and return the first record in the set.
- * The next call of `current()` will get the first record in the set.
+ * Reset the set's iterator and return the first entity in the set.
+ * The next call of `current()` will get the first entity in the set.
*
- * @return object `Record`
+ * @return object Returns the first `Entity` instance in the set.
*/
public function rewind() {
- $this->_valid = (reset($this->_items) !== false);
+ $this->_valid = (reset($this->_data) || count($this->_data));
if (!$this->_valid && !$this->_hasInitialized) {
$this->_hasInitialized = true;
- if ($record = $this->_populate()) {
+ if ($entity = $this->_populate()) {
$this->_valid = true;
- return $record;
+ return $entity;
}
}
+ return current($this->_data);
}
/**
- * Returns meta information for this `RecordSet`
+ * Returns meta information for this `Collection`.
*
* @return array
*/
@@ -104,22 +207,22 @@ abstract class Collection extends \lithium\util\Collection {
}
/**
- * Applies a callback to all items in the collection.
+ * Applies a callback to all data in the collection.
*
- * Overriden to load any data that has not yet been loaded.
+ * Overridden to load any data that has not yet been loaded.
*
* @param callback $filter The filter to apply.
* @return object This collection instance.
*/
public function each($filter) {
- if (!$this->_closed()) {
+ if (!$this->closed()) {
while($this->next()) {}
}
return parent::each($filter);
}
/**
- * Applies a callback to a copy of all items in the collection
+ * Applies a callback to a copy of all data in the collection
* and returns the result.
*
* Overriden to load any data that has not yet been loaded.
@@ -127,27 +230,65 @@ abstract class Collection extends \lithium\util\Collection {
* @param callback $filter The filter to apply.
* @param array $options The available options are:
* - `'collect'`: If `true`, the results will be returned wrapped
- * in a new Collection object or subclass.
- * @return array|object The filtered items.
+ * in a new `Collection` object or subclass.
+ * @return array|object The filtered data.
*/
public function map($filter, array $options = array()) {
- if (!$this->_closed()) {
+ $defaults = array('collect' => true);
+ $options += $defaults;
+
+ if (!$this->closed()) {
while($this->next()) {}
}
- return parent::map($filter, $options);
+ $data = parent::map($filter, $options);
+
+ if ($options['collect']) {
+ foreach (array('_model', '_schema', '_pathKey') as $key) {
+ $data->{$key} = $this->{$key};
+ }
+ }
+ return $data;
}
/**
- * Magic alias for `_close()`. Ensures that the data set's connection is closed when the object
- * is destroyed.
+ * Converts the current state of the data structure to an array.
*
- * @return void
+ * @return array Returns the array value of the data in this `Collection`.
*/
- public function __destruct() {
- $this->_close();
+ public function data() {
+ return $this->to('array');
}
- abstract protected function _populate($data = null, $key = null);
+ /**
+ * Adds the specified object to the `Collection` instance, and assigns associated metadata to
+ * the added object.
+ *
+ * @param string $offset The offset to assign the value to.
+ * @param mixed $data The entity object to add.
+ * @return mixed Returns the set `Entity` object.
+ */
+ public function offsetSet($offset, $data) {
+ if (is_array($data) && ($model = $this->_model)) {
+ $data = $model::connection()->cast($this, $data);
+ } elseif (is_object($data)) {
+ $data->assignTo($this);
+ }
+ return $this->_data[] = $data;
+ }
+
+ /**
+ * Gets the stat or stats associated with this `Collection`.
+ *
+ * @param string $name Stat name.
+ * @return mixed Single stat if `$name` supplied, else all stats for this
+ * `Collection`.
+ */
+ public function stats($name = null) {
+ if ($name) {
+ return isset($this->_stats[$name]) ? $this->_stats[$name] : null;
+ }
+ return $this->_stats;
+ }
/**
* Executes when the associated result resource pointer reaches the end of its data set. The
@@ -155,23 +296,47 @@ abstract class Collection extends \lithium\util\Collection {
*
* @return void
*/
- protected function _close() {
- if (!$this->_closed()) {
- $this->_result = $this->_handle->result('close', $this->_result, $this);
- unset($this->_handle);
+ public function close() {
+ if (!empty($this->_result)) {
+ $this->_result = null;
}
}
/**
- * Checks to see if this record set has already fetched all available records and freed the
+ * Checks to see if this entity has already fetched all available entities and freed the
* associated result resource.
*
- * @return boolean Returns true if all records are loaded and the database resources have been
+ * @return boolean Returns true if all entities are loaded and the database resources have been
* freed, otherwise returns false.
*/
- protected function _closed() {
- return (empty($this->_result) || !isset($this->_handle) || empty($this->_handle));
+ public function closed() {
+ return empty($this->_result);
+ }
+
+ /**
+ * Ensures that the data set's connection is closed when the object is destroyed.
+ *
+ * @return void
+ */
+ public function __destruct() {
+ $this->close();
}
+
+ /**
+ * A method to be implemented by concrete `Collection` classes which, provided a reference to a
+ * backend data source, and a resource representing a query result cursor, fetches new result
+ * data and wraps it in the appropriate object type, which is added into the `Collection` and
+ * returned.
+ *
+ * @param mixed $data Data (in an array or object) that is manually added to the data
+ * collection. If `null`, data is automatically fetched from the associated backend
+ * data source, if available.
+ * @param mixed $key String, integer or array key representing the unique key of the data
+ * object. If `null`, the key will be extracted from the data passed or fetched,
+ * using the associated `Model` class.
+ * @return object Returns a `Record` or `Document` object, or other `Entity` object.
+ */
+ abstract protected function _populate($data = null, $key = null);
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/data/Connections.php b/libraries/lithium/data/Connections.php
index 385b5ef..c030d9c 100644
--- a/libraries/lithium/data/Connections.php
+++ b/libraries/lithium/data/Connections.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data;
-use \Exception;
-use \lithium\util\String;
-use \lithium\core\Libraries;
+use Exception;
+use lithium\util\String;
+use lithium\core\Libraries;
/**
* The `Connections` class manages a list of named configurations that connect to external
@@ -19,7 +19,7 @@ use \lithium\core\Libraries;
*
* While connections can be added and removed dynamically during the course of your application
* (using `Connections::add()`), it is most typical to define all connections at once, in
- * `app/config/connections.php`.
+ * `app/config/bootstrap/connections.php`.
*
* THe `Connections` class handles adapter classes efficiently by only loading adapter classes and
* creating instances when they are requested (using `Connections::get()`).
@@ -33,7 +33,7 @@ class Connections extends \lithium\core\Adaptable {
/**
* A Collection of the configurations you add through Connections::add().
*
- * @var `\lithium\util\Collection`
+ * @var `lithium\util\Collection`
*/
protected static $_configurations = array();
@@ -45,7 +45,7 @@ class Connections extends \lithium\core\Adaptable {
protected static $_adapters = 'data.source';
/**
- * Add connection configurations to your app in `/app/config/connections.php`
+ * Add connection configurations to your app in `/app/config/bootstrap/connections.php`
*
* For example:
* {{{
@@ -101,11 +101,11 @@ class Connections extends \lithium\core\Adaptable {
*/
public static function add($name, array $config = array()) {
$defaults = array(
- 'type' => 'database',
+ 'type' => null,
'adapter' => null,
'host' => 'localhost',
'login' => '',
- 'password' => ''
+ 'password' => '',
);
return static::$_configurations[$name] = $config + $defaults;
}
@@ -140,10 +140,20 @@ class Connections extends \lithium\core\Adaptable {
* @return mixed A configured instance of the connection, or an array of the configuration used.
*/
public static function get($name = null, array $options = array()) {
+ static $mockAdapter;
+
$defaults = array('config' => false, 'autoCreate' => true);
$options += $defaults;
- if (empty($name)) {
+ if ($name === false) {
+ if (!$mockAdapter) {
+ $class = Libraries::locate('data.source', 'Mock');
+ $mockAdapter = new $class();
+ }
+ return $mockAdapter;
+ }
+
+ if (!$name) {
return array_keys(static::$_configurations);
}
@@ -155,7 +165,7 @@ class Connections extends \lithium\core\Adaptable {
}
$settings = static::$_configurations[$name];
- if (!isset($settings[0]['adapter']) || !is_object($settings[0]['adapter'])) {
+ if (!isset($settings[0]['object'])) {
if (!$options['autoCreate']) {
return null;
}
diff --git a/libraries/lithium/data/Entity.php b/libraries/lithium/data/Entity.php
new file mode 100644
index 0000000..33ccb60
--- /dev/null
+++ b/libraries/lithium/data/Entity.php
@@ -0,0 +1,465 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data;
+
+use BadMethodCallException;
+use UnexpectedValueException;
+use lithium\data\Source;
+use lithium\util\Collection as Col;
+
+/**
+ * `Entity` is a smart data object which represents data such as a row or document in a
+ * database. Entities have fields (often known as columns in databases), and track changes to its
+ * fields, as well as associated validation errors, etc.
+ *
+ * The `Entity` class can also be used as a base class for your own custom data objects, and is the
+ * basis for generating forms with the `Form` helper.
+ *
+ * @see lithium\template\helper\Form
+ */
+class Entity extends \lithium\core\Object {
+
+ /**
+ * Fully-namespaced class name of model that this record is bound to. Instance methods declared
+ * in the model may be called on the entity. See the `Model` class documentation for more
+ * information.
+ *
+ * @see lithium\data\Model
+ * @see lithium\data\Entity::__call()
+ * @var string
+ */
+ protected $_model = null;
+
+ /**
+ * Associative array of the entity's fields and values.
+ *
+ * @var array
+ */
+ protected $_data = array();
+
+ /**
+ * An array containing all related records and recordsets, keyed by relationship name, as
+ * defined in the bound model class.
+ *
+ * @var array
+ */
+ protected $_relationships = array();
+
+ /**
+ * If this record is chained off of another, contains the origin object.
+ *
+ * @var object
+ */
+ protected $_parent = null;
+
+ /**
+ * A reference to the object that originated this record set; usually an instance of
+ * `lithium\data\Source` or `lithium\data\source\Database`. Used to load column definitions and
+ * lazy-load records.
+ *
+ * @var object
+ */
+ protected $_handle = null;
+
+ /**
+ * The list of validation errors associated with this object, where keys are field names, and
+ * values are arrays containing one or more validation error messages.
+ *
+ * @see lithium\data\Entity::errors()
+ * @var array
+ */
+ protected $_errors = array();
+
+ /**
+ * Contains the values of updated fields. These values will be persisted to the backend data
+ * store when the document is saved.
+ *
+ * @var array
+ */
+ protected $_updated = array();
+
+ /**
+ * An array of key/value pairs corresponding to fields that should be updated using atomic
+ * incrementing / decrementing operations. Keys match field names, and values indicate the value
+ * each field should be incremented or decrememnted by.
+ *
+ * @see lithium\data\Entity::increment()
+ * @see lithium\data\Entity::decrement()
+ * @var array
+ */
+ protected $_increment = array();
+
+ /**
+ * A flag indicating whether or not this entity exists. Set to `false` if this is a
+ * newly-created entity, or if this entity has been loaded and subsequently deleted. Set to
+ * `true` if the entity has been loaded from the database, or has been created and subsequently
+ * saved.
+ *
+ * @var boolean
+ */
+ protected $_exists = false;
+
+ /**
+ * A local copy of the schema definition. This is the same as `lithium\data\Model::$_schema`,
+ * but can be defined here if this is a one-off object or class used for a single purpose, i.e.
+ * to create a form.
+ *
+ * @var array
+ */
+ protected $_schema = array();
+
+ /**
+ * Auto configuration.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array(
+ 'classes' => 'merge', 'parent', 'schema', 'data', 'model', 'exists', 'pathKey'
+ );
+
+ /**
+ * Creates a new record object with default values.
+ *
+ * Options defined:
+ * - 'data' _array_: Data to enter into the record. Defaults to an empty array.
+ * - 'model' _string_: Class name that provides the data-source for this record.
+ * Defaults to `null`.
+ *
+ * @param array $config
+ * @return object Record object.
+ */
+ public function __construct(array $config = array()) {
+ $defaults = array('model' => null, 'data' => array());
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Overloading for reading inaccessible properties.
+ *
+ * @param string $name Property name.
+ * @return mixed Result.
+ */
+ public function &__get($name) {
+ $data = null;
+ $null = null;
+
+ if (isset($this->_relationships[$name])) {
+ return $this->_relationships[$name];
+ }
+
+ if (($model = $this->_model) && $this->_handle) {
+ foreach ($model::relations() as $relation => $config) {
+ $linkKey = $config->data('fieldName');
+ $type = $config->data('type') == 'hasMany' ? 'set' : 'entity';
+ $class = $this->_classes[$type];
+
+ if ($linkKey === $name) {
+ $data = isset($this->_data[$name]) ? $this->_data[$name] : array();
+ $this->_relationships[$name] = new $class();
+ return $this->_relationships[$name];
+ }
+ }
+ }
+ if (isset($this->_updated[$name])) {
+ return $this->_updated[$name];
+ }
+ if (isset($this->_data[$name])) {
+ return $this->_data[$name];
+ }
+ return $null;
+ }
+
+ /**
+ * Overloading for writing to inaccessible properties.
+ *
+ * @param string $name Property name.
+ * @param string $value Property value.
+ * @return mixed Result.
+ */
+ public function __set($name, $value = null) {
+ if (is_array($name) && !$value) {
+ return array_map(array(&$this, '__set'), array_keys($name), array_values($name));
+ }
+ $this->_updated[$name] = $value;
+ }
+
+ /**
+ * Overloading for calling `isset()` or `empty()` on inaccessible properties.
+ *
+ * @param string $name Property name.
+ * @return mixed Result.
+ */
+ public function __isset($name) {
+ return isset($this->_data[$name]) || isset($this->_updated[$name]);
+ }
+
+ /**
+ * Magic method that allows calling of model methods on this record instance, i.e.:
+ * {{{
+ * $record->validates();
+ * }}}
+ *
+ * @param string $method
+ * @param array $params
+ * @return mixed
+ */
+ public function __call($method, $params) {
+ if (!($model = $this->_model) || !method_exists($model, $method)) {
+ $message = "No model bound or unhandled method call `{$method}`.";
+ throw new BadMethodCallException($message);
+ }
+ array_unshift($params, $this);
+ $class = $model::invokeMethod('_object');
+ return call_user_func_array(array(&$class, $method), $params);
+ }
+
+ /**
+ * Allows several properties to be assigned at once, i.e.:
+ * {{{
+ * $record->set(array('title' => 'Lorem Ipsum', 'value' => 42));
+ * }}}
+ *
+ * @param $values An associative array of fields and values to assign to the `Record`.
+ * @return void
+ */
+ public function set($values) {
+ foreach ($values as $name => $value) {
+ $this->__set($name, $value);
+ }
+ }
+
+ /**
+ * Access the data fields of the record. Can also access a $named field.
+ *
+ * @param string $name Optionally included field name.
+ * @return array|string Entire data array if $name is empty, otherwise the value from the named
+ * field.
+ */
+ public function data($name = null) {
+ if ($name) {
+ return $this->__get($name);
+ }
+ return $this->_updated + $this->_data;
+ }
+
+ /**
+ * Returns the model which this entity is bound to.
+ *
+ * @return string The fully qualified model class name.
+ */
+ public function model() {
+ return $this->_model;
+ }
+
+ public function schema($field = null) {
+ $schema = array();
+
+ switch (true) {
+ case ($this->_schema):
+ $schema = $this->_schema;
+ break;
+ case ($model = $this->_model):
+ $schema = $model::schema();
+ break;
+ }
+ if ($field) {
+ return isset($schema[$field]) ? $schema[$field] : null;
+ }
+ return $schema;
+ }
+
+ /**
+ * Access the errors of the record.
+ *
+ * @see lithium\data\Entity::$_errors
+ * @param array|string $field If an array, overwrites `$this->_errors`. If a string, and
+ * `$value` is not `null`, sets the corresponding key in `$this->_errors` to `$value`.
+ * @param string $value Value to set.
+ * @return array|string Either the `$this->_errors` array, or single value from it.
+ */
+ public function errors($field = null, $value = null) {
+ if ($field === null) {
+ return $this->_errors;
+ }
+ if (is_array($field)) {
+ return ($this->_errors = $field);
+ }
+ if ($value === null && isset($this->_errors[$field])) {
+ return $this->_errors[$field];
+ }
+ if ($value !== null) {
+ return $this->_errors[$field] = $value;
+ }
+ return $value;
+ }
+
+ /**
+ * A flag indicating whether or not this record exists.
+ *
+ * @return boolean `True` if the record was `read` from the data-source, or has been `create`d
+ * and `save`d. Otherwise `false`.
+ */
+ public function exists() {
+ return $this->_exists;
+ }
+
+ /**
+ * Called after an `Entity` is saved. Updates the object's internal state to reflect the
+ * corresponding database entity, and sets the `Entity` object's key, if this is a newly-created
+ * object.
+ *
+ * @param mixed $id The ID to assign, where applicable.
+ * @param array $data Any additional generated data assigned to the object by the database.
+ * @return void
+ */
+ public function update($id = null, array $data = array()) {
+ $this->_exists = true;
+ $model = $this->_model;
+ $key = array();
+
+ if ($id && $model) {
+ $key = $model::meta('key');
+ $key = is_array($key) ? array_combine($key, $id) : array($key => $id);
+ }
+ $this->_data = ($key + $data + $this->_updated + $this->_data);
+ $this->_updated = array();
+ }
+
+ /**
+ * Safely (atomically) increments the value of the specified field by an arbitrary value.
+ * Defaults to `1` if no value is specified. Throws an exception if the specified field is
+ * non-numeric.
+ *
+ * @param string $field The name of the field to be incrememnted.
+ * @param string $value The value to increment the field by. Defaults to `1` if this parameter
+ * is not specified.
+ * @return integer Returns the current value of `$field`, based on the value retrieved from the
+ * data source when the entity was loaded, plus any increments applied. Note that it may
+ * not reflect the most current value in the persistent backend data source.
+ * @throws UnexpectedValueException Throws an exception when `$field` is set to a non-numeric
+ * type.
+ */
+ public function increment($field, $value = 1) {
+ if (!isset($this->_data[$field])) {
+ return $this->_data[$field] = $value;
+ }
+ if (!is_numeric($this->_data[$field])) {
+ throw new UnexpectedValueException("Field '{$field}' cannot be incremented.");
+ }
+ $base = isset($this->_updated[$field]) ? $this->_updated[$field] : $this->_data[$field];
+ return $this->_updated[$field] = ($base + $value);
+ }
+
+ /**
+ * Decrements a field by the specified value. Works identically to `increment()`, but in
+ * reverse.
+ *
+ * @see lithium\data\Entity::increment()
+ * @param string $field The name of the field to decrement.
+ * @param string $value The value by which to decrement the field. Defaults to `1`.
+ * @return integer Returns the new value of `$field`, after modification.
+ */
+ public function decrement($field, $value = 1) {
+ return $this->increment($field, $value * -1);
+ }
+
+ /**
+ * Gets the array of fields modified on this entity.
+ *
+ * @return array Returns an array where the keys are entity field names, and the values are
+ * always `true`.
+ */
+ public function modified() {
+ if (!$this->_exists) {
+ $keys = array_keys($this->_data);
+ return array_combine($keys, array_fill(0, count($keys), true));
+ }
+ return $this->_modified;
+ }
+
+ public function export() {
+ return array(
+ 'exists' => $this->_exists,
+ 'data' => $this->_data,
+ 'update' => $this->_updated,
+ 'increment' => $this->_increment,
+ );
+ }
+
+ /**
+ * Configures protected properties of a `Record` so that it is parented to `$parent`.
+ *
+ * @param object $parent
+ * @param array $config
+ * @return void
+ */
+ public function assignTo($parent, array $config = array()) {
+ foreach ($config as $key => $val) {
+ $this->{'_' . $key} = $val;
+ }
+ $this->_parent =& $parent;
+ }
+
+ /**
+ * Converts the data in the record set to a different format, i.e. an array.
+ *
+ * @param string $format currently only `array`
+ * @param array $options
+ * @return mixed
+ */
+ public function to($format, array $options = array()) {
+ switch ($format) {
+ case 'array':
+ $result = Col::toArray($this->data());
+ break;
+ default:
+ $result = $this;
+ break;
+ }
+ return $result;
+ }
+
+ /**
+ * Instantiates a new `Entity` object as a descendant of the current object, and sets all
+ * default values and internal state.
+ *
+ * @param string $classType The type of class to create, either `'entity'` or `'set'`.
+ * @param string $key The key name to which the related object is assigned.
+ * @param array $data The internal data of the related object.
+ * @param array $options Any other options to pass when instantiating the related object.
+ * @return object Returns a new `Entity` object instance.
+ */
+ protected function _relation($classType, $key, $data, $options = array()) {
+ $parent = $this;
+ $key = ($key === null) ? count($this->_data) : $key;
+ $pathKey = trim("{$this->_pathKey}.{$key}", '.');
+
+ if (($key || $key === 0) && $model = $this->_model) {
+ foreach ($model::relations() as $name => $relation) {
+ if ($key === $relation->data('fieldName')) {
+ $model = $relation->data('to');
+ break;
+ }
+ }
+ }
+
+ if (is_object($data) && method_exists($data, 'assignTo')) {
+ $data->assignTo($this, compact('model', 'pathKey'));
+ return $data;
+ }
+
+ if ($model) {
+ $exists = $this->_exists;
+ $options += compact('parent', 'exists', 'pathKey');
+ return $model::connection()->cast($this, $data, $options);
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/Model.php b/libraries/lithium/data/Model.php
index 88c2436..2d9627e 100644
--- a/libraries/lithium/data/Model.php
+++ b/libraries/lithium/data/Model.php
@@ -2,35 +2,140 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data;
-use \lithium\util\Set;
-use \lithium\util\Inflector;
-use \RuntimeException;
-use \UnexpectedValueException;
+use lithium\util\Set;
+use lithium\util\Inflector;
+use lithium\core\ConfigException;
+use BadMethodCallException;
/**
- * Model class
+ * The `Model` class is the starting point for the domain logic of your application.
+ * Models are tasked with providing meaning to otherwise raw and unprocessed data (e.g.
+ * user profile).
*
- * @todo Methods: bind(), and 'bind' option for find() et al., create(), save(), delete(),
- * validate()
+ * Models expose a consistent and unified API to interact with an underlying datasource (e.g.
+ * MongoDB, CouchDB, MySQL) for operations such as querying, saving, updating and deleting data
+ * from the persistent storage.
+ *
+ * Models allow you to interact with your data in two fundamentally different ways: querying, and
+ * data mutation (saving/updating/deleting). All query-related operations may be done through the
+ * static `find()` method, along with some additional utility methods provided for convenience.
+ *
+ * Classes extending this one should, conventionally, be named as Plural, CamelCase and be
+ * placed in the `app/models` directory. i.e. a posts model would be `app/model/Posts.php`.
+ *
+ * Examples:
+ * {{{
+ * // Return all 'post' records
+ * Posts::find('all');
+ * Posts::all();
+ *
+ * // With conditions and a limit
+ * Posts::find('all', array('conditions' => array('published' => true), 'limit' => 10));
+ * Posts::all(array('conditions' => array('published' => true), 'limit' => 10));
+ *
+ * // Integer count of all 'post' records
+ * Posts::find('count');
+ * Posts::count(); // This is equivalent to the above.
+ *
+ * // With conditions
+ * Posts::find('count', array('conditions' => array('published' => true)));
+ * Posts::count(array('published' => true));
+ * }}}
+ *
+ * The actual objects returned from `find()` calls will depend on the type of data source in use.
+ * MongoDB, for example, will return results as a `Document` (as will CouchDB), while MySQL will
+ * return results as a `RecordSet`. Both of these classes extend a common `lithium\data\Collection`
+ * class, and provide the necessary abstraction to make working with either type completely
+ * transparent.
+ *
+ * For data mutation (saving/updating/deleting), the `Model` class acts as a broker to the proper
+ * objects. When creating a new record or document, for example, a call to `Posts::create()` will
+ * return an instance of `lithium\data\entity\Record` or `lithium\data\entity\Document`, which can
+ * then be acted upon.
+ *
+ * Example:
+ * {{{
+ * $post = Posts::create();
+ * $post->author = 'Robert';
+ * $post->title = 'Newest Post!';
+ * $post->content = 'Lithium rocks. That is all.';
+ *
+ * $post->save();
+ * }}}
+ *
+ * @see lithium\data\entity\Record
+ * @see lithium\data\entity\Document
+ * @see lithium\data\collection\RecordSet
+ * @see lithium\data\collection\DocumentSet
+ * @see lithium\data\Connections
*/
class Model extends \lithium\core\StaticObject {
+ /**
+ * Criteria for data validation.
+ *
+ * Example usage:
+ * {{{
+ * public $validates = array(
+ * 'title' => 'please enter a title',
+ * 'email' => array(
+ * array('notEmpty', 'message' => 'Email is empty.'),
+ * array('email', 'message' => 'Email is not valid.'),
+ * )
+ * );
+ * }}}
+ *
+ * @var array
+ */
public $validates = array();
+ /**
+ * Model hasOne relations.
+ * Not yet implemented.
+ *
+ * @var array
+ */
public $hasOne = array();
+ /**
+ * Model hasMany relations.
+ * Not yet implemented.
+ *
+ * @var array
+ */
public $hasMany = array();
+ /**
+ * Model belongsTo relations.
+ * Not yet implemented.
+ *
+ * @var array
+ */
public $belongsTo = array();
+ /**
+ * Stores model instances for internal use.
+ *
+ * While the `Model` public API does not require instantiation thanks to late static binding
+ * introduced in PHP 5.3, LSB does not apply to class attributes. In order to prevent you
+ * from needing to redeclare every single `Model` class attribute in subclasses, instances of
+ * the models are stored and used internally.
+ *
+ * @var array
+ */
protected static $_instances = array();
+ /**
+ * Stores the filters that are applied to the model instances stored in `Model::$_instances`.
+ *
+ * @var array
+ */
protected $_instanceFilters = array();
/**
@@ -38,20 +143,36 @@ class Model extends \lithium\core\StaticObject {
*
* @var array
*/
- protected $_classes = array(
- 'connections' => '\lithium\data\Connections',
- 'query' => '\lithium\data\model\Query',
- 'validator' => '\lithium\util\Validator'
+ protected static $_classes = array(
+ 'connections' => 'lithium\data\Connections',
+ 'query' => 'lithium\data\model\Query',
+ 'validator' => 'lithium\util\Validator'
);
+ /**
+ * A list of the current relation types for this `Model`.
+ *
+ * @var array
+ */
protected $_relations = array();
+ /**
+ * List of relation types and the configuration fields that these relations
+ * require/accept.
+ *
+ * Valid relation types are:
+ *
+ * - `belongsTo`
+ * - `hasOne`
+ * - `hasMany`
+ *
+ * @var array
+ */
protected $_relationTypes = array(
'belongsTo' => array('class', 'key', 'conditions', 'fields'),
- 'hasOne' => array('class', 'key', 'conditions', 'fields', 'dependent'),
+ 'hasOne' => array('class', 'key', 'conditions', 'fields'),
'hasMany' => array(
- 'class', 'key', 'conditions', 'fields', 'order', 'limit',
- 'dependent', 'exclusive', 'finder', 'counter'
+ 'class', 'key', 'conditions', 'fields', 'order', 'limit'
)
);
@@ -78,16 +199,69 @@ class Model extends \lithium\core\StaticObject {
'name' => null,
'title' => null,
'class' => null,
+ 'locked' => true,
'source' => null,
'connection' => 'default',
'initialized' => false
);
+ /**
+ * Stores the data schema.
+ *
+ * The schema is lazy-loaded by the first call to `Model::schema()`, unless it has been
+ * manually defined in the `Model` subclass.
+ *
+ * For schemaless persistent storage (e.g. MongoDB), this is never populated automatically - if
+ * you desire a fixed schema to interact with in those cases, you will be required to define it
+ * yourself.
+ *
+ * Example:
+ * {{{
+ * protected $_schema = array(
+ * '_id' => array('type' => 'id'), // required for Mongo
+ * 'name' => array('type' => 'string', 'default' => 'Moe', 'null' => false),
+ * 'sign' => array('type' => 'string', 'default' => 'bar', 'null' => false),
+ * 'age' => array('type' => 'integer', 'default' => 0, 'null' => false)
+ * );
+ * }}}
+ *
+ * For MongoDB specifically, you can also implement a callback in your database connection
+ * configuration that fetches and returns the schema data, as in the following:
+ *
+ * {{{
+ * // config/bootstrap/connections.php:
+ * Connections::add('default', array(
+ * 'type' => 'MongoDb',
+ * 'host' => 'localhost',
+ * 'database' => 'app_name',
+ * 'schema' => function($db, $collection, $meta) {
+ * $result = $db->connection->schemas->findOne(compact('collection'));
+ * return $result ? $result['data'] : array();
+ * }
+ * ));
+ * }}}
+ *
+ * This example defines an optional MongoDB convention in which the schema for each individual
+ * collection is stored in a "schemas" collection, where each document contains the name of
+ * a collection, along with a `'data'` key, which contains the schema for that collection, in
+ * the format specified above.
+ *
+ * @see lithium\data\source\MongoDb::$_schema
+ * @var array
+ */
protected $_schema = array();
/**
* Default query parameters.
*
+ * - `conditions`: The conditional query elements, e.g.
+ * `'conditions' => array('published' => true)`
+ * - `fields`: The fields that should be retrieved. When set to `null`, defaults to
+ * all fields.
+ * - `order`: The order in which the data will be returned, e.g. `'order' => 'ASC'`.
+ * - `limit`: The maximum number of records to return.
+ * - `page`: For pagination of data.
+ *
* @var array
*/
protected $_query = array(
@@ -100,136 +274,186 @@ class Model extends \lithium\core\StaticObject {
/**
* Custom find query properties, indexed by name.
+ *
+ * @var array
*/
protected $_finders = array();
/**
+ * List of base model classes. Any classes which are declared to be base model classes (i.e.
+ * extended but not directly interacted with) must be present in this list. Models can declare
+ * themselves as base models using the following code:
+ * {{{
+ * public static function __init() {
+ * static::_isBase(__CLASS__, true);
+ * parent::__init();
+ * }
+ * }}}
+ *
+ * @var array
+ */
+ protected static $_baseClasses = array(__CLASS__ => true);
+
+ /**
* Sets default connection options and connects default finders.
*
* @param array $options
* @return void
* @todo Merge in inherited config from AppModel and other parent classes.
*/
- public static function __init(array $options = array()) {
- if (($class = get_called_class()) == __CLASS__) {
- return;
- }
- $name = static::_name();
- $self = static::_instance();
- $base = get_class_vars(__CLASS__);
-
- $meta = $options + $self->_meta + $base['_meta'];
- $classes = $self->_classes + $base['_classes'];
-
- $conn = $classes['connections']::get($meta['connection']);
- $config = ($conn) ? $conn->configureClass($class) : array();
- $defaults = array('classes' => array(), 'meta' => array(), 'finders' => array());
- $config += $defaults;
-
- $self->_classes = ($config['classes'] + $classes);
- $self->_meta = (compact('class', 'name') + $config['meta'] + $meta);
- $self->_meta['initialized'] = false;
-
- $self->_finders += $config['finders'] + $self->_findFilters();
- static::_instance()->_relations = static::_relations();
+ public static function __init() {
+ static::config();
}
/**
- * Exports an array of custom finders which use the filter system to wrap around `find()`
+ * Configures the model for use. This method is called by `Model::__init()`.
*
+ * This method will set the `Model::$_schema`, `Model::$_meta`, `Model::$_finders` class
+ * attributes, as well as obtain a handle to the configured persistent storage connection.
+ *
+ * @param array $options Possible options are:
+ * - `meta`: Meta-information for this model, such as the connection.
+ * - `finders`: Custom finders for this model.
* @return void
*/
- protected static function _findFilters() {
- $self = static::_instance();
- $query =& $self->_query;
- $classes = $self->_classes;
-
- return array(
- 'first' => function($self, $params, $chain) {
- $params['options']['limit'] = 1;
- $data = $chain->next($self, $params, $chain);
- return is_object($data) ? $data->rewind() : $data;
- },
- 'list' => function($self, $params, $chain) {
- $result = array();
- $meta = $self::meta();
-
- array_map(
- function($record) use (&$result, $meta) {
- $result[$record->{$meta['key']}] = $record->{$meta['title']};
- },
- $chain->next($self, $params, $chain)
- );
- return $result;
- },
- 'count' => function($self, $params, $chain) use (&$query, &$classes) {
- $model = $self;
- $type = $params['type'];
- $options = array_filter($params['options']);
+ public static function config(array $options = array()) {
+ if (static::_isBase($class = get_called_class())) {
+ return;
+ }
+ $self = static::_object();
+ $meta = array();
+ $schema = array();
+ $source = array();
+ $classes = static::$_classes;
+
+ foreach (static::_parents() as $parent) {
+ $parentConfig = get_class_vars($parent);
+
+ foreach (array('meta', 'schema', 'classes') as $key) {
+ if (isset($parentConfig["_{$key}"])) {
+ ${$key} += $parentConfig["_{$key}"];
+ }
+ }
+ if ($parent == __CLASS__) {
+ break;
+ }
+ }
+ $tmp = $options + $self->_meta + $meta;
+ $source = array('meta' => array(), 'finders' => array(), 'schema' => array());
- $classes = $options['classes'];
- unset($options['classes']);
+ if ($tmp['connection']) {
+ $conn = $classes['connections']::get($tmp['connection']);
+ $source = (($conn) ? $conn->configureClass($class) : array()) + $source;
+ }
+ static::$_classes = $classes;
+ $name = static::_name();
- if (!isset($options['conditions']) && $options) {
- $options = array('conditions' => $options) + compact('classes', 'model');
- }
+ $local = compact('class', 'name') + $options + $self->_meta;
+ $self->_meta = ($local + $source['meta'] + $meta);
+ $self->_meta['initialized'] = false;
+ $self->_schema += $schema + $source['schema'];
- $query = new $classes['query'](array('type' => 'read') + $options);
- return $self::invokeMethod('_connection')->calculation('count', $query, $options);
- }
- );
+ $self->_finders += $source['finders'] + $self->_findFilters();
+ static::_relations();
}
+ /**
+ * Allows the use of syntactic-sugar like `Model::all()` instead of `Model::find('all')`.
+ *
+ * @see lithium\data\Model::find()
+ * @see lithium\data\Model::$_meta
+ * @link http://php.net/manual/en/language.oop5.overloading.php PHP Manual: Overloading
+ *
+ * @throws BadMethodCallException On unhandled call, will throw an exception.
+ * @param string $method Method name caught by `__callStatic()`.
+ * @param array $params Arguments given to the above `$method` call.
+ * @return mixed Results of dispatched `Model::find()` call.
+ */
public static function __callStatic($method, $params) {
- $self = static::_instance();
+ $self = static::_object();
+ $isFinder = isset($self->_finders[$method]);
+
+ if ($isFinder && count($params) === 2 && is_array($params[1])) {
+ $params = array($params[1] + array($method => $params[0]));
+ }
- if ($method == 'all' || isset($self->_finders[$method])) {
- if (isset($params[0]) && (is_string($params[0]) || is_int($params[0]))) {
+ if ($method == 'all' || $isFinder) {
+ if ($params && is_scalar($params[0])) {
$params[0] = array('conditions' => array($self->_meta['key'] => $params[0]));
}
return $self::find($method, $params ? $params[0] : array());
}
+ preg_match('/^findBy(?P<field>\w+)$|^find(?P<type>\w+)By(?P<fields>\w+)$/', $method, $args);
- if (preg_match('/^find(?P<type>\w+)By(?P<fields>\w+)/', $method, $match)) {
- $match['type'][0] = strtolower($match['type'][0]);
- $type = $match['type'];
- $fields = Inflector::underscore($match['fields']);
+ if (!$args) {
+ $message = "Method `%s` not defined or handled in class `%s`.";
+ throw new BadMethodCallException(sprintf($message, $method, get_class($self)));
}
+ $field = Inflector::underscore($args['field'] ? $args['field'] : $args['fields']);
+ $type = isset($args['type']) ? $args['type'] : 'first';
+ $type[0] = strtolower($type[0]);
+
+ $conditions = array($field => array_shift($params));
+ $params = (isset($params[0]) && count($params) == 1) ? $params[0] : $params;
+ return $self::find($type, compact('conditions') + $params);
}
/**
- * undocumented function
+ * The `find` method allows you to retrieve data from the connected data source.
+ *
+ * Examples:
+ * {{{
+ * Model::find('all'); // returns all records
+ * Model::find('count'); // returns a count of all records
*
- * @param string $type
- * @param string $options
+ * // The first ten records that have 'author' set to 'Lithium'
+ * Model::find('all', array(
+ * 'conditions' => array('author' => "Lithium"), 'limit' => 10
+ * ));
+ * }}}
+ *
+ * @param string $type The find type, which is looked up in `Model::$_finders`. By default it
+ * accepts `all`, `first`, `list` and `count`,
+ * @param string $options Options for the query. By default, accepts:
+ * - `conditions`: The conditional query elements, e.g.
+ * `'conditions' => array('published' => true)`
+ * - `fields`: The fields that should be retrieved. When set to `null`, defaults to
+ * all fields.
+ * - `order`: The order in which the data will be returned, e.g. `'order' => 'ASC'`.
+ * - `limit`: The maximum number of records to return.
+ * - `page`: For pagination of data.
* @return void
* @filter This method can be filtered.
*/
public static function find($type, array $options = array()) {
- $self = static::_instance();
- $classes = $self->_classes;
+ $self = static::_object();
+ $finder = array();
$defaults = array(
'conditions' => null, 'fields' => null, 'order' => null, 'limit' => null, 'page' => 1
);
- if ($type != 'all' && !isset($self->_finders[$type])) {
+ if ($type === null) {
+ return null;
+ }
+
+ if ($type != 'all' && is_scalar($type) && !isset($self->_finders[$type])) {
$options['conditions'] = array($self->_meta['key'] => $type);
$type = 'first';
}
- $options += ((array) $self->_query + (array) $defaults + compact('classes'));
+ $options = (array) $options + (array) $self->_query + (array) $defaults;
$meta = array('meta' => $self->_meta, 'name' => get_called_class());
$params = compact('type', 'options');
$filter = function($self, $params) use ($meta) {
- $options = $params['options'] + array('model' => $meta['name']);
- $query = $options['classes']['query'];
-
- $connection = $self::invokeMethod('_connection');
- return $connection->read(new $query(array('type' => 'read') + $options), $options);
+ $options = $params['options'] + array('type' => 'read', 'model' => $meta['name']);
+ $query = $self::invokeMethod('_instance', array('query', $options));
+ return $self::connection()->read($query, $options);
};
- $finder = isset($self->_finders[$type]) ? array($self->_finders[$type]) : array();
+ if (is_string($type) && isset($self->_finders[$type])) {
+ $finder = is_callable($self->_finders[$type]) ? array($self->_finders[$type]) : array();
+ }
return static::_filter(__FUNCTION__, $params, $filter, $finder);
}
@@ -237,12 +461,12 @@ class Model extends \lithium\core\StaticObject {
* Gets or sets a finder by name. This can be an array of default query options,
* or a closure that accepts an array of query options, and a closure to execute.
*
- * @param string $name
- * @param string $options
- * @return void
+ * @param string $name The finder name, e.g. `first`.
+ * @param string $options If you are setting a finder, this is the finder definition.
+ * @return mixed Finder definition if querying, null otherwise.
*/
public static function finder($name, $options = null) {
- $self = static::_instance();
+ $self = static::_object();
if (empty($options)) {
return isset($self->_finders[$name]) ? $self->_finders[$name] : null;
@@ -250,10 +474,18 @@ class Model extends \lithium\core\StaticObject {
$self->_finders[$name] = $options;
}
+ /**
+ * Set/get method for `Model` metadata.
+ *
+ * @see lithium\data\Model::$_meta
+ * @param string $key Model metadata key.
+ * @param string $value Model metadata value.
+ * @return mixed Metadata value for a given key.
+ */
public static function meta($key = null, $value = null) {
- $self = static::_instance();
+ $self = static::_object();
- if (!empty($value)) {
+ if ($value) {
$self->_meta[$key] = $value;
}
if (is_array($key)) {
@@ -273,58 +505,96 @@ class Model extends \lithium\core\StaticObject {
return isset($self->_meta[$key]) ? $self->_meta[$key] : null;
}
+ /**
+ * If no values supplied, returns the name of the `Model` key. If values
+ * are supplied, returns the key value.
+ *
+ * @param array $values An array of values.
+ * @return mixed Key value.
+ */
public static function key($values = array()) {
- $key = static::_instance()->_meta['key'];
+ $key = static::_object()->_meta['key'];
if (is_object($values) && method_exists($values, 'to')) {
$values = $values->to('array');
- } elseif (is_object($values) && isset($values->{$key})) {
+ } elseif (is_object($values) && is_string($key) && isset($values->{$key})) {
return $values->{$key};
}
- if (empty($values)) {
+ if (!$values) {
return $key;
}
- if (is_array($key)) {
- $scope = array_combine($key, array_fill(0, count($key), null));
- return array_intersect_key($values, $scope);
+ if (!is_array($values) && !is_array($key)) {
+ return array($key => $values);
}
- return isset($values[$key]) ? $values[$key] : null;
+ $key = (array) $key;
+ return array_intersect_key($values, array_combine($key, $key));
}
+ /**
+ * Returns a list of models related to `Model`, or a list of models related
+ * to this model, but of a certain type.
+ *
+ * @param string $name A type of model relation.
+ * @return array An array of relation types.
+ */
public static function relations($name = null) {
- $self = static::_instance();
+ $self = static::_object();
- if (empty($name)) {
- return array_keys($self->_relations);
+ if (!$name) {
+ return $self->_relations;
}
-
if (isset($self->_relationTypes[$name])) {
- return $self->$name;
+ return array_keys(array_filter($self->_relations, function($i) use ($name) {
+ return $i->data('type') == $name;
+ }));
}
+ return isset($self->_relations[$name]) ? $self->_relations[$name] : null;
+ }
- foreach (array_keys($self->_relationTypes) as $type) {
- if (isset($self->{$type}[$name])) {
- return $self->{$type}[$name];
- }
+ /**
+ * Creates a relationship binding between this model and another.
+ *
+ * @see lithium\data\model\Relationship
+ * @param string $type The type of relationship to create. Must be one of `'hasOne'`,
+ * `'hasMany'` or `'belongsTo'`.
+ * @param string $name The name of the relationship. If this is also the name of the model,
+ * the model must be in the same namespace as this model. Otherwise, the
+ * fully-namespaced path to the model class must be specified in `$config`.
+ * @param array $config Any other configuration that should be specified in the relationship.
+ * See the `Relationship` class for more information.
+ * @return object Returns an instance of the `Relationship` class that defines the connection.
+ */
+ public static function bind($type, $name, array $config = array()) {
+ $self = static::_object();
+
+ if (!isset($self->_relationTypes[$type])) {
+ throw new ConfigException("Invalid relationship type `{$type}` specified.");
}
- return null;
+ $rel = static::connection()->relationship(get_called_class(), $type, $name, $config);
+ return static::_object()->_relations[$name] = $rel;
}
/**
* Lazy-initialize the schema for this Model object, if it is not already manually set in the
* object. You can declare `protected $_schema = array(...)` to define the schema manually.
*
- * @param string $field Optional. You may pass a field name to get schema information for just
- * one field. Otherwise, an array containing all fields is returned.
+ * @param mixed $field Optional. You may pass a field name to get schema information for just
+ * one field. Otherwise, an array containing all fields is returned. If `false`, the
+ * schema is reset to an empty value. If an array, field definitions contained are
+ * appended to the schema.
* @return array
*/
public static function schema($field = null) {
- $self = static::_instance();
- if (empty($self->_schema)) {
- $self->_schema = $self->_connection()->describe($self::meta('source'), $self->_meta);
+ $self = static::_object();
+
+ if ($field === false) {
+ return $self->_schema = array();
}
- if (is_string($field) && !empty($field)) {
+ if (!$self->_schema) {
+ $self->_schema = static::connection()->describe($self::meta('source'), $self->_meta);
+ }
+ if (is_string($field) && $field) {
return isset($self->_schema[$field]) ? $self->_schema[$field] : null;
}
return $self->_schema;
@@ -350,23 +620,47 @@ class Model extends \lithium\core\StaticObject {
return false;
}
$schema = static::schema();
- return (!empty($schema) && isset($schema[$field]));
+ return ($schema && isset($schema[$field]));
}
/**
- * Instantiates a new record object, initialized with any data passed in. For example:
+ * Instantiates a new record or document object, initialized with any data passed in. For
+ * example:
+ *
* {{{
- * $post = Post::create(array("title" => "New post"));
+ * $post = Posts::create(array("title" => "New post"));
* echo $post->title; // echoes "New post"
- * $post->save();
+ * $success = $post->save();
+ * }}}
+ *
+ * Note that while this method creates a new object, there is no effect on the database until
+ * the `save()` method is called.
+ *
+ * In addition, this method can be used to simulate loading a pre-existing object from the
+ * database, without actually querying the database:
+ *
+ * {{{
+ * $post = Posts::create(array("id" => $id, "moreData" => "foo"), array("exists" => true));
+ * $post->title = "New title";
+ * $success = $post->save();
* }}}
*
- * @param array $data Any data that this record should be populated with initially.
- * @return object Returns a new, **un-saved** record object.
+ * This will create an update query against the object with an ID matching `$id`. Also note that
+ * only the `title` field will be updated.
+ *
+ * @param array $data Any data that this object should be populated with initially.
+ * @param array $options Options to be passed to item.
+ * @return object Returns a new, _un-saved_ record or document object. In addition to the values
+ * passed to `$data`, the object will also contain any values assigned to the
+ * `'default'` key of each field defined in `$_schema`.
*/
- public static function create(array $data = array()) {
- return static::_filter(__FUNCTION__, compact('data'), function($self, $params) {
+ public static function create(array $data = array(), array $options = array()) {
+ $self = static::_object();
+ $params = compact('data', 'options');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
$data = $params['data'];
+ $options = $params['options'];
if ($schema = $self::schema()) {
foreach ($schema as $field => $config) {
@@ -375,102 +669,246 @@ class Model extends \lithium\core\StaticObject {
}
}
}
- return $self::invokeMethod('_connection')->item($self, $data);
+ return $self::connection()->item($self, $data, $options);
});
}
/**
* An instance method (called on record and document objects) to create or update the record or
- * document in the database that corresponds to `$record`. For example:
+ * document in the database that corresponds to `$entity`.
+ *
+ * For example, to create a new record or document:
* {{{
- * $post = Post::create();
+ * $post = Posts::create(); // Creates a new object, which doesn't exist in the database yet
* $post->title = "My post";
+ * $success = $post->save();
+ * }}}
+ *
+ * It is also used to update existing database objects, as in the following:
+ * {{{
+ * $post = Posts::first($id);
+ * $post->title = "Revised title";
+ * $success = $post->save();
+ * }}}
+ *
+ * By default, an object's data will be checked against the validation rules of the model it is
+ * bound to. Any validation errors that result can then be accessed through the `errors()`
+ * method.
+ *
+ * {{{
+ * if (!$post->save($someData)) {
+ * return array('errors' => $post->errors());
+ * }
+ * }}}
+ *
+ * To override the validation checks and save anyway, you can pass the `'validate'` option:
+ *
+ * {{{
+ * $post->title = "We Don't Need No Stinkin' Validation";
+ * $post->body = "I know what I'm doing.";
* $post->save(null, array('validate' => false));
* }}}
*
- * @param object $record The record or document object to be saved in the database.
+ * @see lithium\data\Model::$validates
+ * @see lithium\data\Model::validates()
+ * @see lithium\data\Model::errors()
+ * @param object $entity The record or document object to be saved in the database. This
+ * parameter is implicit and should not be passed under normal circumstances.
+ * In the above example, the call to `save()` on the `$post` object is
+ * transparently proxied through to the `Posts` model class, and `$post` is passed
+ * in as the `$entity` parameter.
* @param array $data Any data that should be assigned to the record before it is saved.
* @param array $options Options:
- * - 'callbacks': If `false`, all callbacks will be disabled before executing. Defaults to
- * `true`.
- * - 'validate': If `false`, validation will be skipped, and the record will be immediately
- * saved. Defaults to `true`.
- * - 'whitelist': An array of fields that are allowed to be saved to this record.
+ * - `'callbacks'` _boolean_: If `false`, all callbacks will be disabled before
+ * executing. Defaults to `true`.
+ * - `'validate'` _mixed_: If `false`, validation will be skipped, and the record will
+ * be immediately saved. Defaults to `true`. May also be specified as an array, in
+ * which case it will replace the default validation rules specified in the
+ * `$validates` property of the model.
+ * - `'whitelist'` _array_: An array of fields that are allowed to be saved to this
+ * record.
*
* @return boolean Returns `true` on a successful save operation, `false` on failure.
*/
- public function save($record, $data = null, array $options = array()) {
- $self = static::_instance();
- $classes = $self->_classes;
- $meta = array('model' => get_called_class()) + $self->_meta;
+ public function save($entity, $data = null, array $options = array()) {
+ $self = static::_object();
+ $_meta = array('model' => get_called_class()) + $self->_meta;
+ $_schema = $self->_schema;
- $defaults = array('validate' => true, 'whitelist' => null, 'callbacks' => true);
- $options += $defaults + compact('classes');
- $params = compact('record', 'data', 'options');
+ $defaults = array(
+ 'validate' => true,
+ 'whitelist' => null,
+ 'callbacks' => true,
+ 'locked' => $self->_meta['locked'],
+ );
+ $options += $defaults;
+ $params = compact('entity', 'data', 'options');
- $filter = function($self, $params) use ($meta) {
- $record = $params['record'];
+ $filter = function($self, $params) use ($_meta, $_schema) {
+ $entity = $params['entity'];
$options = $params['options'];
if ($params['data']) {
- $record->set($params['data']);
+ $entity->set($params['data']);
}
-
- if ($options['validate'] && !$record->validates()) {
- return false;
+ if ($rules = $options['validate']) {
+ if (!$entity->validates(is_array($rules) ? compact('rules') : array())) {
+ return false;
+ }
+ }
+ if (($whitelist = $options['whitelist']) || $options['locked']) {
+ $whitelist = $whitelist ?: array_keys($_schema);
}
- $type = $record->exists() ? 'update' : 'create';
- $queryOptions = compact('type') + $options + $meta + compact('record');
- $query = new $options['classes']['query']($queryOptions);
- return $self::invokeMethod('_connection')->{$type}($query, $options);
+ $type = $entity->exists() ? 'update' : 'create';
+ $queryOpts = compact('type', 'whitelist', 'entity') + $options + $_meta;
+ $query = $self::invokeMethod('_instance', array('query', $queryOpts));
+ return $self::connection()->{$type}($query, $options);
};
if (!$options['callbacks']) {
- return $filter->__invoke($record, $options);
+ return $filter(get_called_class(), $params);
}
return static::_filter(__FUNCTION__, $params, $filter);
}
- public function validates($record, array $options = array()) {
- $self = static::_instance();
- $validator = $self->_classes['validator'];
- $params = compact('record', 'options');
+ /**
+ * An important part of describing the business logic of a model class is defining the
+ * validation rules. In Lithium models, rules are defined through the `$validates` class
+ * property, and are used by this method before saving to verify the correctness of the data
+ * being sent to the backend data source.
+ *
+ * Note that these are application-level validation rules, and do not
+ * interact with any rules or constraints defined in your data source. If such constraints fail,
+ * an exception will be thrown by the database layer. The `validates()` method only checks
+ * against the rules defined in application code.
+ *
+ * This method uses the `Validator` class to perform data validation. An array representation of
+ * the entity object to be tested is passed to the `check()` method, along with the model's
+ * validation rules. Any rules defined in the `Validator` class can be used to validate fields.
+ * See the `Validator` class to add custom rules, or override built-in rules.
+ *
+ * @see lithium\data\Model::$validates
+ * @see lithium\util\Validator::check()
+ * @see lithium\data\Entity::errors()
+ * @param string $entity Model entity to validate. Typically either a `Record` or `Document`
+ * object. In the following example:
+ * {{{
+ * $post = Posts::create($data);
+ * $success = $post->validates();
+ * }}}
+ * The `$entity` parameter is equal to the `$post` object instance.
+ * @param array $options Available options:
+ * - `'rules'` _array_: If specified, this array will _replace_ the default
+ * validation rules defined in `$validates`.
+ * - `'events'` _mixed_: A string or array defining one or more validation
+ * _events_. Events are different contexts in which data events can occur, and
+ * correspond to the optional `'on'` key in validation rules. For example, by
+ * default, `'events'` is set to either `'create'` or `'update'`, depending on
+ * whether `$entity` already exists. Then, individual rules can specify
+ * `'on' => 'create'` or `'on' => 'update'` to only be applied at certain times.
+ * Using this parameter, you can set up custom events in your rules as well, such
+ * as `'on' => 'login'`. Note that when defining validation rules, the `'on'` key
+ * can also be an array of multiple events.
+ * @return boolean Returns `true` if all validation rules on all fields succeed, otherwise
+ * `false`. After validation, the messages for any validation failures are assigned to
+ * the entity, and accessible through the `errors()` method of the entity object.
+ */
+ public function validates($entity, array $options = array()) {
+ $defaults = array(
+ 'rules' => $this->validates,
+ 'events' => $entity->exists() ? 'update' : 'create',
+ );
+ $options += $defaults;
+ $self = static::_object();
+ $validator = static::$_classes['validator'];
+ $params = compact('entity', 'options');
$filter = function($parent, $params) use (&$self, $validator) {
- extract($params);
+ $entity = $params['entity'];
+ $options = $params['options'];
+ $rules = $options['rules'];
+ unset($options['rules']);
- if ($errors = $validator::check($record->data(), $self->validates, $options)) {
- $record->errors($errors);
+ if ($errors = $validator::check($entity->data(), $rules, $options)) {
+ $entity->errors($errors);
}
return empty($errors);
};
return static::_filter(__FUNCTION__, $params, $filter);
}
- public function delete($record, array $options = array()) {
- $self = static::_instance();
- $query = $self->_classes['query'];
- $model = get_called_class();
- $params = compact('record', 'options');
- $method = __FUNCTION__;
-
- return static::_filter($method, $params, function($self, $params) use ($model, $query) {
- extract($params);
- $options += compact('record', 'model');
- return $self::invokeMethod('_connection')->delete(new $query($options), $options);
+ /**
+ * Deletes the data associated with the current `Model`.
+ *
+ * @param object $entity Entity to delete.
+ * @param array $options Options.
+ * @return boolean Success.
+ */
+ public function delete($entity, array $options = array()) {
+ $self = static::_object();
+ $params = compact('entity', 'options');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $options = $params + $params['options'] + array('model' => $self, 'type' => 'delete');
+ unset($options['options']);
+
+ $query = $self::invokeMethod('_instance', array('query', $options));
+ return $self::connection()->delete($query, $options);
});
}
/**
- * Gets just the class name portion of a fully-name-spaced class name, i.e.
- * `app\models\Post::_name()` returns `'Post'`.
+ * Update multiple records or documents with the given data, restricted by the given set of
+ * criteria (optional).
*
- * @return string
+ * @param mixed $data Typically an array of key/value pairs that specify the new data with which
+ * the records will be updated. For SQL databases, this can optionally be an SQL
+ * fragment representing the `SET` clause of an `UPDATE` query.
+ * @param mixed $conditions An array of key/value pairs representing the scope of the records
+ * to be updated.
+ * @param array $options Any database-specific options to use when performing the operation. See
+ * the `delete()` method of the corresponding backend database for available
+ * options.
+ * @return boolean Returns `true` if the update operation succeeded, otherwise `false`.
*/
- protected static function _name() {
- static $name;
- return $name ?: $name = join('', array_slice(explode("\\", get_called_class()), -1));
+ public static function update($data, $conditions = array(), array $options = array()) {
+ $self = static::_object();
+ $params = compact('data', 'conditions', 'options');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $options = $params + $params['options'] + array('model' => $self, 'type' => 'update');
+ unset($options['options']);
+
+ $query = $self::invokeMethod('_instance', array('query', $options));
+ return $self::connection()->update($query, $options);
+ });
+ }
+
+ /**
+ * Remove multiple documents or records based on a given set of criteria. **WARNING**: If no
+ * criteria are specified, or if the criteria (`$conditions`) is an empty value (i.e. an empty
+ * array or `null`), all the data in the backend data source (i.e. table or collection) _will_
+ * be deleted.
+ *
+ * @param mixed $conditions An array of key/value pairs representing the scope of the records or
+ * documents to be deleted.
+ * @param array $options Any database-specific options to use when performing the operation. See
+ * the `delete()` method of the corresponding backend database for available
+ * options.
+ * @return boolean Returns `true` if the remove operation succeeded, otherwise `false`.
+ */
+ public static function remove($conditions = array(), array $options = array()) {
+ $self = static::_object();
+ $params = compact('conditions', 'options');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $options = $params['options'] + $params + array('model' => $self, 'type' => 'delete');
+ unset($options['options']);
+
+ $query = $self::invokeMethod('_instance', array('query', $options));
+ return $self::connection()->delete($query, $options);
+ });
}
/**
@@ -480,18 +918,25 @@ class Model extends \lithium\core\StaticObject {
* @return object Returns an instance of `lithium\data\Source` from the connection configuration
* to which this model is bound.
*/
- protected static function &_connection() {
- $self = static::_instance();
- $connections = $self->_classes['connections'];
+ public static function &connection() {
+ $self = static::_object();
+ $connections = static::$_classes['connections'];
$name = isset($self->_meta['connection']) ? $self->_meta['connection'] : null;
- if (!$name) {
- throw new UnexpectedValueException("Connection name not defined");
- }
if ($conn = $connections::get($name)) {
return $conn;
}
- throw new RuntimeException("The data connection {$name} is not configured");
+ throw new ConfigException("The data connection `{$name}` is not configured.");
+ }
+
+ /**
+ * Gets just the class name portion of a fully-name-spaced class name, i.e.
+ * `app\models\Posts::_name()` returns `'Posts'`.
+ *
+ * @return string
+ */
+ protected static function _name() {
+ return basename(str_replace('\\', '/', get_called_class()));
}
/**
@@ -502,7 +947,7 @@ class Model extends \lithium\core\StaticObject {
* @param mixed $closure
*/
public static function applyFilter($method, $closure = null) {
- $instance = static::_instance();
+ $instance = static::_object();
$methods = (array) $method;
foreach ($methods as $method) {
@@ -528,7 +973,7 @@ class Model extends \lithium\core\StaticObject {
$method = get_called_class() . '::' . $method;
}
list($class, $method) = explode('::', $method, 2);
- $instance = static::_instance();
+ $instance = static::_object();
if (isset($instance->_instanceFilters[$method])) {
$filters = array_merge($instance->_instanceFilters[$method], $filters);
@@ -536,7 +981,7 @@ class Model extends \lithium\core\StaticObject {
return parent::_filter($method, $params, $callback, $filters);
}
- protected static function &_instance() {
+ protected static function &_object() {
$class = get_called_class();
if (!isset(static::$_instances[$class])) {
@@ -552,22 +997,83 @@ class Model extends \lithium\core\StaticObject {
* @todo See if this can be rewritten to be lazy.
*/
protected static function _relations() {
- $relations = array();
- $self = static::_instance();
+ $self = static::_object();
+
+ if (!$self->_meta['connection']) {
+ return;
+ }
foreach ($self->_relationTypes as $type => $keys) {
- foreach (Set::normalize($self->{$type}) as $name => $options) {
- $key = Inflector::underscore($type == 'belongsTo' ? $name : $self->_meta['name']);
- $defaults = array(
- 'type' => $type,
- 'class' => $name,
- 'fields' => true,
- 'key' => $key . '_id'
- );
- $relations[$name] = (array) $options + $defaults;
+ foreach (Set::normalize($self->{$type}) as $name => $config) {
+ static::bind($type, $name, (array) $config);
}
}
- return $relations;
+ }
+
+ /**
+ * Helper function for setting/getting base class settings.
+ *
+ * @param string $class Classname.
+ * @param boolean $set If `true`, then the `$class` will be set.
+ * @return boolean Success.
+ */
+ protected static function _isBase($class = null, $set = false) {
+ if ($set) {
+ static::$_baseClasses[$class] = true;
+ }
+ return isset(static::$_baseClasses[$class]);
+ }
+
+ /**
+ * Exports an array of custom finders which use the filter system to wrap around `find()`.
+ *
+ * @return void
+ */
+ protected static function _findFilters() {
+ $self = static::_object();
+ $_query = $self->_query;
+
+ return array(
+ 'first' => function($self, $params, $chain) {
+ $params['options']['limit'] = 1;
+ $data = $chain->next($self, $params, $chain);
+ $data = is_object($data) ? $data->rewind() : $data;
+ return $data ?: null;
+ },
+ 'list' => function($self, $params, $chain) {
+ $result = array();
+ $meta = $self::meta();
+ $name = $meta['key'];
+
+ foreach ($chain->next($self, $params, $chain) as $entity) {
+ $key = $entity->{$name};
+ $result[is_scalar($key) ? $key : (string) $key] = $entity->{$meta['title']};
+ }
+ return $result;
+ },
+ 'count' => function($self, $params) use ($_query) {
+ $model = $self;
+ $type = $params['type'];
+ $options = array_diff_key($params['options'], $_query);
+
+ if ($options && !isset($params['options']['conditions'])) {
+ $options = array('conditions' => $options);
+ } else {
+ $options = $params['options'];
+ }
+ $options += array('type' => 'read') + compact('model');
+ $query = $self::invokeMethod('_instance', array('query', $options));
+ return $self::connection()->calculation('count', $query, $options);
+ }
+ );
+ }
+
+ /**
+ * @deprecated
+ * @see lithium\data\Model::connection()
+ */
+ protected static function &_connection() {
+ return static::connection();
}
}
diff --git a/libraries/lithium/data/Source.php b/libraries/lithium/data/Source.php
index a48ba92..052c645 100644
--- a/libraries/lithium/data/Source.php
+++ b/libraries/lithium/data/Source.php
@@ -2,12 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data;
+use lithium\core\NetworkException;
+
/**
* This is the base class for Lithium's data abstraction layer.
*
@@ -23,12 +25,31 @@ namespace lithium\data;
abstract class Source extends \lithium\core\Object {
/**
+ * The list of object properties to be automatically assigned from configuration passed to
+ * `__construct()`.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array('classes' => 'merge');
+
+ /**
+ * Default entity and set classes used by subclasses of `Source`.
+ *
+ * @var array
+ */
+ protected $_classes = array(
+ 'entity' => 'lithium\data\Entity',
+ 'set' => 'lithium\data\Collection',
+ 'relationship' => 'lithium\data\model\Relationship'
+ );
+
+ /**
* Stores a connection to a remote resource. Usually a database connection (`resource` type),
* or an HTTP connection object ('object' type).
*
* @var mixed
*/
- protected $_connection = null;
+ public $connection = null;
/**
* Stores the status of this object's connection. Updated when `connect()` or `disconnect()` are
@@ -49,7 +70,7 @@ abstract class Source extends \lithium\core\Object {
*/
public function __construct(array $config = array()) {
$defaults = array('autoConnect' => true);
- parent::__construct((array) $config + $defaults);
+ parent::__construct($config + $defaults);
}
/**
@@ -88,15 +109,23 @@ abstract class Source extends \lithium\core\Object {
$options += $defaults;
if (!$this->_isConnected && $options['autoConnect']) {
- $this->connect();
+ try {
+ $this->connect();
+ } catch (NetworkException $e) {
+ $this->_isConnected = false;
+ }
}
return $this->_isConnected;
}
- public function run($query, array $options = array()) {
- if (is_object($query) && method_exists($this, $query->type())) {
- return $this->{$query->type()}($query, $options);
- }
+ /**
+ * Quotes data-source-native identifiers, where applicable.
+ *
+ * @param string $name Identifier name.
+ * @return string Returns `$name`, quoted if applicable.
+ */
+ public function name($name) {
+ return $name;
}
/**
@@ -118,7 +147,26 @@ abstract class Source extends \lithium\core\Object {
*/
abstract public function entities($class = null);
- abstract public function describe($entity, $meta = array());
+ /**
+ * Gets the column schema for a given entity (such as a database table).
+ *
+ * @param mixed $entity Specifies the table name for which the schema should be returned, or
+ * the class name of the model object requesting the schema, in which case the model
+ * class will be queried for the correct table name.
+ * @param array $meta
+ * @return array Returns an associative array describing the given table's schema, where the
+ * array keys are the available fields, and the values are arrays describing each
+ * field, containing the following keys:
+ * - `'type'`: The field type name
+ */
+ abstract public function describe($entity, array $meta = array());
+
+ /**
+ * Defines or modifies the default settings of a relationship between two models.
+ *
+ * @return array Returns an array containing the configuration for a model relationship.
+ */
+ abstract public function relationship($class, $type, $name, array $options = array());
/**
* Abstract. Must be defined by child classes.
@@ -160,6 +208,26 @@ abstract class Source extends \lithium\core\Object {
abstract public function delete($query, array $options = array());
/**
+ * Casts data into proper format when added to a collection or entity object.
+ *
+ * @param mixed $entity The entity or collection for which data is being cast, or the name of
+ * the model class to which the entity/collection is bound.
+ * @param array $data An array of data being assigned.
+ * @param array $options Any associated options with, for example, instantiating new objects in
+ * which to wrap the data. Options implemented by `cast()` itself:
+ * - `first` _boolean_: Used when only one value is passed to `cast()`. Even though
+ * that value must be wrapped in an array, setting the `'first'` option to `true`
+ * causes only that one value to be returned.
+ * @return mixed Returns the value of `$data`, cast to the proper format according to the schema
+ * definition of the model class specified by `$model`.
+ */
+ public function cast($entity, array $data, array $options = array()) {
+ $defaults = array('first' => false);
+ $options += $defaults;
+ return $options['first'] ? reset($data) : $data;
+ }
+
+ /**
* Returns the list of methods which format values imported from `Query` objects. Should be
* overridden in subclasses.
*
@@ -173,18 +241,40 @@ abstract class Source extends \lithium\core\Object {
/**
* A method which can be optionally implemented to configure a model class.
*
+ * @see lithium\data\Model::$_meta
+ * @see lithium\data\Model::$_finders
+ * @see lithium\data\Model::$_classes
* @param string $class The name of the model class to be configured.
* @return array This method should return an array one or more of the following keys: `'meta'`,
* `'classes'` or `'finders'`. These keys maps to the three corresponding properties in
* `lithium\data\Model`, and are used to override the base-level default settings and
* dependencies.
- * @see lithium\data\Model::$_meta
- * @see lithium\data\Model::$_finders
- * @see lithium\data\Model::$_classes
*/
public function configureClass($class) {
return array();
}
+
+ /**
+ * This method is responsible for factorying a new instance of a single entity object of correct
+ * type, matching the current data source class.
+ *
+ * @param string $model A fully-namespaced class name representing the model class to which the
+ * `Entity` object will be bound.
+ * @param array $data The default data with which the new `Entity` should be populated.
+ * @param array $options Any additional options to pass to the `Entity`'s constructor
+ * @return object Returns a new, un-saved `Entity` object bound to the model class specified
+ * in `$model`.
+ */
+ public function item($model, array $data = array(), array $options = array()) {
+ $defaults = array('class' => 'entity');
+ $options += $defaults;
+
+ $type = $options['class'];
+ $class = isset($this->_classes[$type]) ? $this->_classes[$type] : $type;
+ unset($options['class']);
+
+ return new $class(compact('model', 'data') + $options);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/data/collection/Document.php b/libraries/lithium/data/collection/Document.php
deleted file mode 100644
index b3dfc65..0000000
--- a/libraries/lithium/data/collection/Document.php
+++ /dev/null
@@ -1,386 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\data\collection;
-
-use \Iterator;
-
-/**
- * `Document` is an alternative to the `collection\RecordSet` class, which is optimized for
- * organizing collections of records from document-oriented databases such as CouchDB or MongoDB.
- * A `Document` object's fields can represent a collection of both simple and complex data types,
- * as well as other `Document` objects. Given the following data (document) structure:
- *
- * {{{
- * {
- * _id: 12345.
- * name: 'Acme, Inc.',
- * employees: {
- * 'Larry': { email: 'larry@acme.com' },
- * 'Curly': { email: 'curly@acme.com' },
- * 'Moe': { email: 'moe@acme.com' }
- * }
- * }
- * }}}
- *
- * You can query the object as follows:
- *
- * {{{$acme = Company::find(12345);}}}
- *
- * This returns a `Document` object, populated with the raw representation of the data.
- *
- * {{{print_r($acme->to('array'));
- *
- * // Yields:
- * // array(
- * // '_id' => 12345,
- * // 'name' => 'Acme, Inc.',
- * // 'employees' => array(
- * // 'Larry' => array('email' => 'larry@acme.com'),
- * // 'Curly' => array('email' => 'curly@acme.com'),
- * // 'Moe' => array('email' => 'moe@acme.com')
- * // )
- * //)}}}
- *
- * As with other database objects, a `Document` exposes its fields as object properties, like so:
- *
- * {{{echo $acme->name; // echoes 'Acme, Inc.'}}}
- *
- * However, accessing a field containing a data set will return that data set wrapped in a
- * sub-`Document` object., i.e.:
- *
- * {{{$employees = $acme->employees;
- * // returns a Document object with the data in 'employees'}}}
- */
-class Document extends \lithium\data\Collection {
-
- /**
- * A reference to this object's parent `Document` object.
- *
- * @var object
- */
- protected $_parent = null;
-
- /**
- * Indicates whether this document has already been created in the database.
- *
- * @var boolean
- */
- protected $_exists = false;
-
- protected $_errors = array();
-
- /**
- * Contains an array of backend-specific statistics generated by the query that produced this
- * `Document` object. These stats are accessible via the `stats()` method.
- *
- * @see lithium\data\collection\Document::stats()
- * @var array
- */
- protected $_stats = array();
-
- /**
- * The class dependencies for `Document`.
- *
- * @var array
- */
- protected $_classes = array(
- 'record' => __CLASS__,
- 'recordSet' => __CLASS__
- );
-
- protected $_autoConfig = array(
- 'items', 'classes' => 'merge', 'handle', 'model',
- 'result', 'query', 'parent', 'exists', 'stats'
- );
-
- public function __construct($config = array()) {
- if (isset($config['data']) && !isset($config['items'])) {
- $config['items'] = $config['data'];
- unset($config['data']);
- }
- parent::__construct($config);
- $this->_items = (array) $this->_items;
- }
-
- /**
- * Magic PHP method used when model method is called on document instance.
- * If no model is set returns `null`.
- *
- * @param $method
- * @param $params
- * @return mixed
- */
- public function __call($method, $params = array()) {
- if (!$model = $this->_model) {
- return null;
- }
- array_unshift($params, $this);
- $class = $model::invokeMethod('_instance');
- return call_user_func_array(array(&$class, $method), $params);
- }
-
- /**
- * PHP magic method used when accessing fields as document properties, i.e. `$document->_id`.
- *
- * @param $name The field name, as specified with an object property.
- * @return mixed Returns the value of the field specified in `$name`, and wraps complex data
- * types in sub-`Document` objects.
- */
- public function __get($name) {
- $items = null;
-
- if (!isset($this->_items[$name]) && !$items = $this->_populate(null, $name)) {
- return null;
- }
- $items = $items ?: $this->_items[$name];
-
- if ($this->_isComplexType($items) && !$items instanceof Iterator) {
- $this->_items[$name] = $this->_record('recordSet', $this->_items[$name]);
- }
- return $this->_items[$name];
- }
-
- /**
- * PHP magic method used when setting properties on the `Document` instance, i.e.
- * `$document->title = 'Lorem Ipsum'`. If `$value` is a complex data type (i.e. associative
- * array), it is wrapped in a sub-`Document` object before being appended.
- *
- * @param $name The name of the field/property to write to, i.e. `title` in the above example.
- * @param $value The value to write, i.e. `'Lorem Ipsum'`.
- * @return void
- */
- public function __set($name, $value = null) {
- if (is_array($name) && empty($value)) {
- $this->_items = $name + $this->_items;
- return;
- }
- if ($this->_isComplexType($value) && !$value instanceof Iterator) {
- $value = $this->_record('recordSet', $value);
- }
- $this->_items[$name] = $value;
- }
-
- /**
- * PHP magic method used to check the presence of a field as document properties, i.e.
- * `$document->_id`.
- *
- * @param $name The field name, as specified with an object property.
- * @return boolean True if the field specified in `$name` exists, false otherwise.
- */
- public function __isset($name) {
- return isset($this->_items[$name]);
- }
-
- /**
- * PHP magic method used when unset() is called on a `Document` instance.
- * Use case for this would be when you wish to edit a document and remove a field, ie. :
- * {{{ $doc = Post::find($id); unset($doc->fieldName); $doc->save(); }}}
- *
- * @param unknown_type $name
- * @return unknown_type
- */
- public function __unset($name) {
- unset($this->_items[$name]);
- }
-
- /**
- * Allows several properties to be assigned at once.
- *
- * For example:
- * {{{
- * $doc->set(array('title' => 'Lorem Ipsum', 'value' => 42));
- * }}}
- *
- * @param $values An associative array of fields and values to assign to the `Document`.
- * @return void
- */
- public function set($values) {
- $this->__set($values);
- }
-
- /**
- * Allows document fields to be accessed as array keys, i.e. `$document['_id']`.
- *
- * @param mixed $offset String or integer indicating the offset or index of a document in a set,
- * or the name of a field in an individual document.
- * @return mixed Returns either a sub-object in the document, or a scalar field value.
- */
- public function offsetGet($offset) {
- return $this->__get($offset);
- }
-
- /**
- * Rewinds the collection of sub-`Document`s to the beginning and returns the first one found.
- *
- * @return object Returns the first `Document` object instance in the collection.
- */
- public function rewind() {
- return ($record = parent::rewind()) ? $record : $this->__get(key($this->_items));
- }
-
- /**
- * Returns the next record in the set, and advances the object's internal pointer. If the end
- * of the set is reached, a new record will be fetched from the data source connection handle
- * (`$_handle`). If no more records can be fetched, returns `null`.
- *
- * @return object|null Returns the next record in the set, or `null`, if no more records are
- * available.
- */
- public function next() {
- $prev = key($this->_items);
- $this->_valid = (next($this->_items) !== false);
- $cur = key($this->_items);
-
- if (!$this->_valid && $cur !== $prev && $cur !== null) {
- $this->_valid = true;
- }
- $this->_valid = $this->_valid ?: !is_null($this->_populate());
- return $this->_valid ? $this->__get(key($this->_items)) : null;
- }
-
- /**
- * Returns `true` if the `Document` object already exists in the database, or `false` if this
- * object is newly-instantiated (i.e. holds a record that has not yet been saved).
- *
- * @return boolean
- */
- public function exists() {
- return $this->_exists;
- }
-
- /**
- * Access the errors of the record.
- *
- * @param array|string $field If an array, overwrites `$this->_errors`. If a string, and $value
- * is not null, sets the corresponding key in $this->_errors to $value
- * @param string $value Value to set.
- * @return array|string Either the $this->_errors array, or single value from it.
- */
- public function errors($field = null, $value = null) {
- if ($field === null) {
- return $this->_errors;
- }
- if (is_array($field)) {
- $this->_errors = $field;
- return $this->_errors;
- }
- if ($value === null && isset($this->_errors[$field])) {
- return $this->_errors[$field];
- }
- if ($value !== null) {
- $this->_errors[$field] = $value;
- }
- return $value;
- }
-
- /**
- * Gets the raw data associated with this `Document`, or single item if `$field` is defined.
- *
- * @param string $field if included will only return the named item
- * @return array Returns a raw array of `Document` data, or individual field value
- */
- public function data($field = null) {
- if ($field) {
- return isset($this->_items[$field]) ? $this->_items[$field] : null;
- }
- return $this->to('array');
- }
-
- public function stats($name = null) {
- if ($name) {
- return isset($this->_stats[$name]) ? $this->_stats[$name] : null;
- }
- return $this->_stats;
- }
-
- /**
- * Called after a `Document` is saved. Updates the object's internal state to reflect the
- * corresponding database record, and sets the `Document`'s primary key, if this is a
- * newly-created object.
- *
- * @param $id The ID to assign, where applicable.
- * @return void
- */
- public function update($id = null, $data = array()) {
- $this->__set($data);
- if ($id) {
- $id = (array) $id;
- $model = $this->_model;
- foreach ((array) $model::meta('key') as $i => $key) {
- $this->__set($key, $id[$i]);
- }
- }
- $this->_exists = true;
- }
-
- /**
- * Used by getter and setter methods to determine whether the value of data is a complex type
- * that should be given its own sub-object withih the `Document`.
- *
- * @param mixed $data The data to be tested. This test is used to determine if `$data` should be
- * wrapped in an instance of `Document`.
- * @return boolean Returns `false` if the value of `$data` is a scalar type or a one-dimensional
- * array of scalar values, otherwise returns `true`.
- */
- protected function _isComplexType($data) {
- if (is_object($data) && (array) $data === array()) {
- return false;
- }
- if (is_scalar($data) || !$data) {
- return false;
- }
- if (is_array($data)) {
- if (array_keys($data) === range(0, count($data) - 1)) {
- if (array_filter($data, 'is_scalar') == array_filter($data)) {
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * Lazy-loads document records from a query using a reference to a database adapter and a query
- * result resource.
- *
- * @param array $items
- * @param mixed $key
- * @return array
- */
- protected function _populate($items = null, $key = null) {
- if ($this->_closed()) {
- return;
- }
- $items = $items ?: $this->_handle->result('next', $this->_result, $this);
-
- if (!isset($items)) {
- return $this->_close();
- }
- return $this->_items[] = $this->_record('record', $items);
- }
-
- /**
- * Instantiates a new `Document` record object as a descendant of the current object, and sets
- * all default values and internal state.
- *
- * @param string $classType The type of class to create, either `'record'` or `'recordSet'`.
- * @param array $items
- * @param array $options
- * @return object Returns a new `Document` object instance.
- */
- protected function _record($classType, $items, $options = array()) {
- $parent = $this;
- $model = $this->_model;
- $exists = $this->_exists;
- $options += compact('model', 'items', 'parent', 'exists');
- return new $this->_classes[$classType]($options);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/data/collection/DocumentArray.php b/libraries/lithium/data/collection/DocumentArray.php
new file mode 100644
index 0000000..3cd1931
--- /dev/null
+++ b/libraries/lithium/data/collection/DocumentArray.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\collection;
+
+use lithium\data\Source;
+use lithium\util\Collection;
+
+class DocumentArray extends \lithium\data\Collection {
+
+ protected $_exists = false;
+
+ /**
+ * Holds an array of values that should be processed on initialization.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array(
+ 'data', 'model', 'result', 'query', 'parent', 'stats', 'pathKey', 'exists'
+ );
+
+ public function exists() {
+ return $this->_exists;
+ }
+
+ public function update($id = null, array $data = array()) {
+ $this->_exists = true;
+ $this->_data = $data ?: $this->_data;
+ }
+
+ /**
+ * Adds conversions checks to ensure certain class types and embedded values are properly cast.
+ *
+ * @param string $format Currently only `array` is supported.
+ * @param array $options
+ * @return mixed
+ */
+ public function to($format, array $options = array()) {
+ $defaults = array('handlers' => array('MongoId' => function($value) {
+ return (string) $value;
+ }));
+
+ if ($format == 'array') {
+ $options += $defaults;
+ return Collection::toArray($this->_data, $options);
+ }
+ return parent::to($format, $options);
+ }
+
+ /**
+ * PHP magic method used to check the presence of a field as document properties, i.e.
+ * `$document->_id`.
+ *
+ * @param $name The field name, as specified with an object property.
+ * @return boolean Returns `true` if the field specified in `$name` exists, otherwise `false`.
+ */
+ public function __isset($name) {
+ return isset($this->_data[$name]);
+ }
+
+ /**
+ * PHP magic method used when unset() is called on a `Document` instance.
+ * Use case for this would be when you wish to edit a document and remove a field, i.e.:
+ * {{{
+ * $doc = Post::find($id);
+ * unset($doc->fieldName);
+ * $doc->save();
+ * }}}
+ *
+ * @param unknown_type $name
+ * @return void
+ */
+ public function __unset($name) {
+ unset($this->_data[$name]);
+ }
+
+ /**
+ * Returns the value at specified offset.
+ *
+ * @param string $offset The offset to retrieve.
+ * @return mixed Value at offset.
+ */
+ public function offsetGet($offset) {
+ return isset($this->_data[$offset]) ? $this->_data[$offset] : null;
+ }
+
+ public function offsetSet($offset, $data) {
+ if ($model = $this->_model) {
+ $options = array('first' => true, 'schema' => $model::schema());
+ $data = $model::connection()->cast($this, array($this->_pathKey => $data), $options);
+ }
+ if ($offset) {
+ return $this->_data[$offset] = $data;
+ }
+ return $this->_data[] = $data;
+ }
+
+ /**
+ * Rewinds the collection of sub-`Document`s to the beginning and returns the first one found.
+ *
+ * @return object Returns the first `Document` object instance in the collection.
+ */
+ public function rewind() {
+ $data = parent::rewind();
+ $key = key($this->_data);
+ return $this->offsetGet($key);
+ }
+
+ public function current() {
+ return $this->offsetGet(key($this->_data));
+ }
+
+ /**
+ * Returns the next document in the set, and advances the object's internal pointer. If the end
+ * of the set is reached, a new document will be fetched from the data source connection handle
+ * (`$_handle`). If no more documents can be fetched, returns `null`.
+ *
+ * @return object|null Returns the next document in the set, or `null`, if no more documents are
+ * available.
+ */
+ public function next() {
+ $prev = key($this->_data);
+ $this->_valid = (next($this->_data) !== false);
+ $cur = key($this->_data);
+
+ if (!$this->_valid && $cur !== $prev && $cur !== null) {
+ $this->_valid = true;
+ }
+ return $this->_valid ? $this->offsetGet(key($this->_data)) : null;
+ }
+
+ public function export() {
+ return array(
+ 'exists' => $this->_exists,
+ 'key' => $this->_pathKey,
+ 'data' => $this->_data,
+ );
+ }
+
+ protected function _populate($data = null, $key = null) {}
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/collection/DocumentSet.php b/libraries/lithium/data/collection/DocumentSet.php
new file mode 100644
index 0000000..54a91d8
--- /dev/null
+++ b/libraries/lithium/data/collection/DocumentSet.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\collection;
+
+class DocumentSet extends \lithium\data\Collection {
+
+ /**
+ * PHP magic method used when setting properties on the `Document` instance, i.e.
+ * `$document->title = 'Lorem Ipsum'`. If `$value` is a complex data type (i.e. associative
+ * array), it is wrapped in a sub-`Document` object before being appended.
+ *
+ * @param $name The name of the field/property to write to, i.e. `title` in the above example.
+ * @param $value The value to write, i.e. `'Lorem Ipsum'`.
+ * @return void
+ */
+ public function __set($name, $value = null) {
+ if (is_array($name) && !$value) {
+ foreach ($name as $key => $value) {
+ $this->__set($key, $value);
+ }
+ return;
+ }
+
+ if (is_string($name) && strpos($name, '.')) {
+ $current = $this;
+ $path = explode('.', $name);
+ $length = count($path) - 1;
+
+ for ($i = 0; $i < $length; $i++) {
+ $key = $path[$i];
+ $next = $current->__get($key);
+
+ if (!is_object($next) && ($model = $this->_model)) {
+ $next = $model::connection()->cast($this, $next);
+ $current->_data[$key] = $next;
+ }
+ $current = $next;
+ }
+ $current->__set(end($path), $value);
+ }
+
+ if (is_array($value)) {
+ $value = $this->_relation('set', $name, $value);
+ }
+ $this->_data[$name] = $value;
+ }
+
+ /**
+ * PHP magic method used to check the presence of a field as document properties, i.e.
+ * `$document->_id`.
+ *
+ * @param $name The field name, as specified with an object property.
+ * @return boolean True if the field specified in `$name` exists, false otherwise.
+ */
+ public function __isset($name) {
+ return isset($this->_data[$name]);
+ }
+
+ /**
+ * PHP magic method used when unset() is called on a `Document` instance.
+ * Use case for this would be when you wish to edit a document and remove a field, ie. :
+ * {{{ $doc = Post::find($id); unset($doc->fieldName); $doc->save(); }}}
+ *
+ * @param unknown_type $name
+ * @return unknown_type
+ */
+ public function __unset($name) {
+ unset($this->_data[$name]);
+ }
+
+ /**
+ * Allows several properties to be assigned at once.
+ *
+ * For example:
+ * {{{
+ * $doc->set(array('title' => 'Lorem Ipsum', 'value' => 42));
+ * }}}
+ *
+ * @param $values An associative array of fields and values to assign to the `Document`.
+ * @return void
+ */
+ public function set($values) {
+ foreach ($values as $key => $val) {
+ $this[$key] = $val;
+ }
+ }
+
+ /**
+ * Allows document fields to be accessed as array keys, i.e. `$document['_id']`.
+ *
+ * @param mixed $offset String or integer indicating the offset or index of a document in a set,
+ * or the name of a field in an individual document.
+ * @return mixed Returns either a sub-object in the document, or a scalar field value.
+ */
+ public function offsetGet($offset) {
+ $data = null;
+ $null = null;
+ $model = $this->_model;
+
+ if (!isset($this->_data[$offset]) && !$data = $this->_populate(null, $offset)) {
+ return $null;
+ }
+ if (is_array($data = $this->_data[$offset]) && $model) {
+ $this->_data[$offset] = $model::connection()->cast($this, $data);
+ }
+ if (isset($this->_data[$offset])) {
+ return $this->_data[$offset];
+ }
+ return $null;
+ }
+
+ /**
+ * Rewinds the collection of sub-`Document`s to the beginning and returns the first one found.
+ *
+ * @return object Returns the first `Document` object instance in the collection.
+ */
+ public function rewind() {
+ $data = parent::rewind() ?: $this->_populate();
+ $key = key($this->_data);
+
+ if (is_object($data)) {
+ return $data;
+ }
+
+ if (isset($this->_data[$key])) {
+ return $this->offsetGet($key);
+ }
+ }
+
+ public function current() {
+ return $this->offsetGet(key($this->_data));
+ }
+
+ /**
+ * Returns the next document in the set, and advances the object's internal pointer. If the end
+ * of the set is reached, a new document will be fetched from the data source connection handle
+ * If no more documents can be fetched, returns `null`.
+ *
+ * @return object|null Returns the next document in the set, or `null`, if no more documents are
+ * available.
+ */
+ public function next() {
+ $prev = key($this->_data);
+ $this->_valid = (next($this->_data) !== false);
+ $cur = key($this->_data);
+
+ if (!$this->_valid && $cur !== $prev && $cur !== null) {
+ $this->_valid = true;
+ }
+ $this->_valid = $this->_valid ?: !is_null($this->_populate());
+ return $this->_valid ? $this->offsetGet(key($this->_data)) : null;
+ }
+
+ public function export(array $options = array()) {
+ $map = function($doc) use ($options) {
+ return is_array($doc) ? $doc : $doc->export();
+ };
+ return array_map($map, $this->_data);
+ }
+
+ /**
+ * Lazy-loads a document from a query using a reference to a database adapter and a query
+ * result resource.
+ *
+ * @param array $data
+ * @param mixed $key
+ * @return array
+ */
+ protected function _populate($data = null, $key = null) {
+ if ($this->closed() || !($model = $this->_model)) {
+ return;
+ }
+ $conn = $model::connection();
+
+ if (($data = $data ?: $this->_result->next()) === null) {
+ return $this->close();
+ }
+ $options = array('exists' => true, 'first' => true, 'pathKey' => $this->_pathKey);
+ return $this->_data[] = $conn->cast($this, array($key => $data), $options);
+ }
+
+ /**
+ * Instantiates a new `Document` object as a descendant of the current object, and sets all
+ * default values and internal state.
+ *
+ * @param string $classType The type of class to create, either `'entity'` or `'set'`.
+ * @param string $key The key name to which the related object is assigned.
+ * @param array $data The internal data of the related object.
+ * @param array $options Any other options to pass when instantiating the related object.
+ * @return object Returns a new `Document` object instance.
+ */
+ protected function _relation($classType, $key, $data, $options = array()) {
+ $parent = $this;
+ $model = $this->_model;
+
+ if (is_object($data) && $data instanceof Document) {
+ $data->assignTo($this, compact('model', 'pathKey'));
+ return $data;
+ }
+ $options += compact('model', 'data', 'parent');
+ return new $this->_classes[$classType]($options);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/collection/RecordSet.php b/libraries/lithium/data/collection/RecordSet.php
index a333bea..4298e92 100644
--- a/libraries/lithium/data/collection/RecordSet.php
+++ b/libraries/lithium/data/collection/RecordSet.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -38,28 +38,23 @@ class RecordSet extends \lithium\data\Collection {
protected $_columns = array();
/**
- * Dynamic dependancies
+ * Initializes the record set and uses the database connection to get the column list contained
+ * in the query that created this object.
*
- * @var array
- */
- protected $_classes = array(
- 'record' => '\lithium\data\model\Record'
- );
-
- /**
- * Initializes the record set and uses the database handle to get the column list contained in
- * the query that created this object.
- *
- * @return void
* @see lithium\data\collection\RecordSet::$_columns
+ * @return void
* @todo The part that uses _handle->schema() should be rewritten so that the column list
* is coming from the query object.
*/
protected function _init() {
parent::_init();
- if ($this->_handle && $this->_result) {
- $this->_columns = $this->_handle->schema($this->_query, $this->_result, $this);
+ if ($this->_result) {
+ $this->_columns = $this->_columnMap();
+ }
+ if ($this->_data && !$this->_index) {
+ $this->_index = array_keys($this->_data);
+ $this->_data = array_values($this->_data);
}
}
@@ -69,9 +64,9 @@ class RecordSet extends \lithium\data\Collection {
* records until either all available records have been loaded, or a matching key has been
* found.
*
+ * @see lithium\data\collection\RecordSet::offsetGet()
* @param mixed $offset The ID of the record to check for.
* @return boolean Returns true if the record's ID is found in the set, otherwise false.
- * @see lithium\data\collection\RecordSet::offsetGet()
*/
public function offsetExists($offset) {
if (in_array($offset, $this->_index)) {
@@ -89,18 +84,18 @@ class RecordSet extends \lithium\data\Collection {
* Because record data in `RecordSet` is lazy-loaded from the database, new records are fetched
* until one with a matching key is found.
*
+ * @see lithium\data\collection\RecordSet::$_index
* @param mixed $offset The offset, or ID (index) of the record you wish to load. If
* `$offset` is `null`, all records are loaded into the record set, and
* `offsetGet` returns `null`.
* @return object Returns a `Record` object if a record is found with a key that matches the
* value of `$offset`, otheriwse returns `null`.
- * @see lithium\data\collection\RecordSet::$_index
*/
public function offsetGet($offset) {
- if (!is_null($offset) && in_array($offset, $this->_index)) {
- return $this->_items[array_search($offset, $this->_index)];
+ if ($offset !== null && in_array($offset, $this->_index)) {
+ return $this->_data[array_search($offset, $this->_index)];
}
- if ($this->_closed()) {
+ if ($this->closed()) {
return null;
}
$model = $this->_model;
@@ -110,28 +105,18 @@ class RecordSet extends \lithium\data\Collection {
return $record;
}
}
- $this->_close();
+ $this->close();
}
/**
* Assigns a value to the specified offset.
*
* @param integer $offset The offset to assign the value to.
- * @param mixed $value The value to set.
+ * @param mixed $data The value to set.
* @return mixed The value which was set.
*/
- public function offsetSet($offset, $value) {
- $class = $this->_classes['record'];
- $model = $this->_model;
-
- if (is_array($value)) {
- $value = new $class(array('data' => $value, 'exists' => true));
- }
- if (array_key_exists($offset, $this->_index)) {
- return $this->_items[array_search($offset, $this->_index)] = $value;
- }
- $this->_index[] = $offset;
- return $this->_items[] = $value;
+ public function offsetSet($offset, $data) {
+ return $this->_populate($data, $offset);
}
/**
@@ -142,7 +127,7 @@ class RecordSet extends \lithium\data\Collection {
*/
public function offsetUnset($offset) {
unset($this->_index[$index = array_search($offset, $this->_index)]);
- unset($this->_items[$index]);
+ unset($this->_data[$index]);
}
/**
@@ -158,7 +143,7 @@ class RecordSet extends \lithium\data\Collection {
if ($record = parent::rewind()) {
return $record;
}
- return empty($this->_items) ? null : $this->_items[$this->_pointer];
+ return empty($this->_data) ? null : $this->_data[$this->_pointer];
}
/**
@@ -167,39 +152,40 @@ class RecordSet extends \lithium\data\Collection {
* @return `Record`
*/
public function current() {
- return $this->_items[$this->_pointer];
+ return $this->_data[$this->_pointer];
}
/**
* Returns the currently pointed to record's unique key.
*
+ * @param boolean $full If true, returns the complete key.
* @return mixed
*/
- public function key() {
- return $this->_index[$this->_pointer];
+ public function key($full = false) {
+ $key = $this->_index[$this->_pointer];
+ return (is_array($key) && !$full) ? reset($key) : $key;
}
/**
* Returns the next record in the set, and advances the object's internal pointer. If the end of
- * the set is reached, a new record will be fetched from the data source connection handle
- * (`$_handle`). If no more records can be fetched, returns `null`.
+ * the set is reached, a new record will be fetched from the data source connection handle.
+ * If no more records can be fetched, returns `null`.
*
* @return object Returns the next record in the set, or `null`, if no more records are
* available.
*/
public function next() {
- $this->_valid = (next($this->_items) !== false && next($this->_index) !== false);
+ $this->_valid = (next($this->_data) !== false && next($this->_index) !== false);
if (!$this->_valid) {
$this->_valid = !is_null($this->_populate());
}
-
$return = null;
+
if ($this->_valid) {
- $return = $this->current();
$this->_pointer++;
+ $return = $this->current();
}
-
return $return;
}
@@ -219,9 +205,13 @@ class RecordSet extends \lithium\data\Collection {
switch ($format) {
case 'array':
- $result = array_map(function($r) { return $r->to('array'); }, $this->_items);
+ $result = array_map(function($r) { return $r->to('array'); }, $this->_data);
if (is_scalar(current($this->_index)) && $options['indexed']) {
- $result = array_combine($this->_index, $result);
+ if (!empty($this->_index) && !empty($result)) {
+ $result = array_combine($this->_index, $result);
+ } else {
+ $result = array();
+ }
}
break;
default:
@@ -232,7 +222,7 @@ class RecordSet extends \lithium\data\Collection {
}
/**
- * Applies a callback to all items in the collection.
+ * Applies a callback to all data in the collection.
*
* Overriden to load any data that has not yet been loaded.
*
@@ -245,7 +235,7 @@ class RecordSet extends \lithium\data\Collection {
}
/**
- * Applies a callback to a copy of all items in the collection
+ * Applies a callback to a copy of all data in the collection
* and returns the result.
*
* Overriden to load any data that has not yet been loaded.
@@ -254,7 +244,7 @@ class RecordSet extends \lithium\data\Collection {
* @param array $options The available options are:
* - `'collect'`: If `true`, the results will be returned wrapped
* in a new `Collection` object or subclass.
- * @return array|object The filtered items.
+ * @return array|object The filtered data.
*/
public function map($filter, array $options = array()) {
$this->offsetGet(null);
@@ -269,28 +259,55 @@ class RecordSet extends \lithium\data\Collection {
* @param mixed $key
* @return array
*/
- protected function _populate($record = null, $key = null) {
- if ($this->_closed()) {
+ protected function _populate($data = null, $key = null) {
+ if ($this->closed() && !$data || !($model = $this->_model)) {
return;
}
- $record = $record ?: $this->_handle->result('next', $this->_result, $this);
- $modelClass = $this->_model;
+ $conn = $model::connection();
- if (!$record) {
- return $this->_close();
+ if (!($data = $data ?: $this->_result->next())) {
+ return $this->close();
}
+ $key = null;
+ $offset = 0;
+ $recordMap = is_object($data) ? array($model => $data) : array();
- foreach ((array) $this->_columns as $model => $fields) {
+ if (!$recordMap) {
+ foreach ($this->_columns as $model => $fields) {
+ $record = array_combine($fields, array_slice($data, $offset, count($fields)));
- if (is_array($record)) {
- $class = $this->_classes['record'];
- $data = array_combine($fields, array_slice($record, 0, count($fields)));
- $record = new $class(compact('model', 'data') + array('exists' => true));
+ if ($model == $this->_model) {
+ $key = $key = $model::key($record);
+ }
+ $recordMap[$model] = $conn->item($model, $record, array('exists' => true));
}
- $this->_items[] = $record;
- $this->_index[] = $modelClass::key($record);
- return $record;
+ } else {
+ $key = $model::key(reset($recordMap));
+ }
+ $record = reset($recordMap);
+ unset($recordMap[key($recordMap)]);
+
+ if (is_array($key)) {
+ $key = count($key) === 1 ? reset($key) : $key;
+ }
+ if (in_array($key, $this->_index)) {
+ $index = array_search($key, $this->_index);
+ $this->_data[$index] = $record;
+ return $this->_data[$index];
+ }
+ $this->_data[] = $record;
+ $this->_index[] = $key;
+ return $record;
+ }
+
+ protected function _columnMap() {
+ if ($this->_query && $map = $this->_query->map()) {
+ return $map;
+ }
+ if (!($model = $this->_model)) {
+ return array();
}
+ return $model::connection()->schema($this->_query, $this->_result, $this);
}
}
diff --git a/libraries/lithium/data/entity/Document.php b/libraries/lithium/data/entity/Document.php
new file mode 100644
index 0000000..d699271
--- /dev/null
+++ b/libraries/lithium/data/entity/Document.php
@@ -0,0 +1,501 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\entity;
+
+use lithium\util\Collection;
+use UnexpectedValueException;
+
+/**
+ * `Document` is an alternative to the `entity\Record` class, which is optimized for
+ * organizing collections of entities from document-oriented databases such as CouchDB or MongoDB.
+ * A `Document` object's fields can represent a collection of both simple and complex data types,
+ * as well as other `Document` objects. Given the following data (document) structure:
+ *
+ * {{{
+ * {
+ * _id: 12345.
+ * name: 'Acme, Inc.',
+ * employees: {
+ * 'Larry': { email: 'larry@acme.com' },
+ * 'Curly': { email: 'curly@acme.com' },
+ * 'Moe': { email: 'moe@acme.com' }
+ * }
+ * }
+ * }}}
+ *
+ * You can query the object as follows:
+ *
+ * {{{$acme = Company::find(12345);}}}
+ *
+ * This returns a `Document` object, populated with the raw representation of the data.
+ *
+ * {{{print_r($acme->to('array'));
+ *
+ * // Yields:
+ * // array(
+ * // '_id' => 12345,
+ * // 'name' => 'Acme, Inc.',
+ * // 'employees' => array(
+ * // 'Larry' => array('email' => 'larry@acme.com'),
+ * // 'Curly' => array('email' => 'curly@acme.com'),
+ * // 'Moe' => array('email' => 'moe@acme.com')
+ * // )
+ * //)}}}
+ *
+ * As with other database objects, a `Document` exposes its fields as object properties, like so:
+ *
+ * {{{echo $acme->name; // echoes 'Acme, Inc.'}}}
+ *
+ * However, accessing a field containing a data set will return that data set wrapped in a
+ * sub-`Document` object., i.e.:
+ *
+ * {{{$employees = $acme->employees;
+ * // returns a Document object with the data in 'employees'}}}
+ */
+class Document extends \lithium\data\Entity implements \Iterator, \ArrayAccess {
+
+ /**
+ * If this `Document` instance has a parent document (see `$_parent`), this value indicates
+ * the key name of the parent document that contains it.
+ *
+ * @see lithium\data\entity\Document::$_parent
+ * @var string
+ */
+ protected $_pathKey = null;
+
+ /**
+ * An array containing all related documents, keyed by relationship name, as defined in the
+ * bound model class.
+ *
+ * @var array
+ */
+ protected $_relations = array();
+
+ /**
+ * Contains an array of removed fields, where the field names are the keys, and the values are
+ * always `true`.
+ *
+ * @var array
+ */
+ protected $_removed = array();
+
+ /**
+ * Contains an array of backend-specific statistics generated by the query that produced this
+ * `Document` object. These stats are accessible via the `stats()` method.
+ *
+ * @see lithium\data\collection\DocumentSet::stats()
+ * @var array
+ */
+ protected $_stats = array();
+
+ /**
+ * Holds the current iteration state. Used by `Document::valid()` to terminate `foreach` loops
+ * when there are no more fields to iterate over.
+ *
+ * @var boolean
+ */
+ protected $_valid = false;
+
+ protected function _init() {
+ parent::_init();
+ $data = (array) $this->_data;
+ $this->_data = array();
+ $this->set($data);
+ $exists = $this->_exists;
+
+ $this->_data = $this->_updated;
+ $this->_updated = array();
+ $this->update();
+ $this->_exists = $exists;
+ unset($this->_autoConfig);
+ }
+
+ /**
+ * PHP magic method used when accessing fields as document properties, i.e. `$document->_id`.
+ *
+ * @param $name The field name, as specified with an object property.
+ * @return mixed Returns the value of the field specified in `$name`, and wraps complex data
+ * types in sub-`Document` objects.
+ */
+ public function &__get($name) {
+ $data = null;
+ $null = null;
+ $model = $this->_model;
+ $conn = $model ? $model::connection() : null;
+
+ if (isset($this->_relationships[$name])) {
+ return $this->_relationships[$name];
+ }
+ if (strpos($name, '.')) {
+ return $this->_getNested($name);
+ }
+
+ if (isset($this->_removed[$name])) {
+ return $null;
+ }
+ if (isset($this->_updated[$name])) {
+ return $this->_updated[$name];
+ }
+
+ if ($model && $conn) {
+ foreach ($model::relations() as $relation => $config) {
+ if ($config && (($linkKey = $config->data('fieldName')) === $name)) {
+ $data = isset($this->_data[$name]) ? $this->_data[$name] : array();
+ $this->_relationships[$name] = $this->_relationship($config);
+ return $this->_relationships[$name];
+ }
+ }
+ if (!isset($this->_data[$name]) && $schema = $model::schema($name)) {
+ $schema = array($name => $schema);
+ $pathKey = $this->_pathKey ? $this->_pathKey : null;
+ $options = compact('pathKey', 'schema') + array('first' => true);
+
+ if (($value = $conn->cast($this, array($name => null), $options)) !== null) {
+ $this->_data[$name] = $value;
+ return $this->_data[$name];
+ }
+ }
+ }
+ if (isset($this->_data[$name])) {
+ return $this->_data[$name];
+ }
+ return $null;
+ }
+
+ public function export() {
+ foreach ($this->_updated as $key => $val) {
+ if (is_a($val, __CLASS__)) {
+ $path = $this->_pathKey ? "{$this->_pathKey}." : '';
+ $this->_updated[$key]->_pathKey = "{$path}{$key}";
+ $this->_updated[$key]->_exists = false;
+ }
+ }
+ return parent::export() + array('key' => $this->_pathKey, 'remove' => $this->_removed);
+ }
+
+ public function update($id = null, array $data = array()) {
+ parent::update($id, $data);
+
+ foreach ($this->_data as $key => $val) {
+ if (is_object($val) && method_exists($val, 'update')) {
+ $this->_data[$key]->update(null, isset($data[$key]) ? $data[$key] : array());
+ }
+ }
+ $this->_removed = array();
+ }
+
+ /**
+ * Instantiates a new `Document` object as a descendant of the current object, and sets all
+ * default values and internal state.
+ *
+ * @param string $classType The type of class to create, either `'entity'` or `'set'`.
+ * @param string $key The key name to which the related object is assigned.
+ * @param array $data The internal data of the related object.
+ * @param array $options Any other options to pass when instantiating the related object.
+ * @return object Returns a new `Document` object instance.
+ */
+ protected function _relation($classType, $key, $data, $options = array()) {
+ $options['exists'] = false;
+ return parent::_relation($classType, $key, $data, $options);
+ }
+
+ protected function _relationship($relationship) {
+ $classType = ($relationship->type == 'hasMany') ? 'set' : 'entity';
+ $config = array('model' => $relationship->to, 'parent' => $this, 'exists' => true);
+ $class = $this->_classes[$classType];
+
+ switch ($relationship->link) {
+ case $relationship::LINK_EMBEDDED:
+ $field = $relationship->fieldName;
+ $config['data'] = isset($this->_data[$field]) ? $this->_data[$field] : array();
+ break;
+ }
+ return new $class($config);
+ }
+
+ protected function &_getNested($name) {
+ $current =& $this;
+ $null = null;
+ $path = explode('.', $name);
+ $length = count($path) - 1;
+
+ foreach ($path as $i => $key) {
+ if (is_array($current)) {
+ $current =& $current[$key];
+ } elseif (isset($current->{$key})) {
+ $current =& $current->{$key};
+ } else {
+ return $null;
+ }
+
+ if (is_scalar($current) && $i < $length) {
+ return $null;
+ }
+ }
+ return $current;
+ }
+
+ /**
+ * PHP magic method used when setting properties on the `Document` instance, i.e.
+ * `$document->title = 'Lorem Ipsum'`. If `$value` is a complex data type (i.e. associative
+ * array), it is wrapped in a sub-`Document` object before being appended.
+ *
+ * @param $name The name of the field/property to write to, i.e. `title` in the above example.
+ * @param $value The value to write, i.e. `'Lorem Ipsum'`.
+ * @return void
+ */
+ public function __set($name, $value = null) {
+ if (is_array($name)) {
+ foreach ($name as $key => $val) {
+ $this->__set($key, $val);
+ }
+ return;
+ }
+ if (is_string($name) && strpos($name, '.')) {
+ return $this->_setNested($name, $value);
+ }
+ if ($model = $this->_model) {
+ $pathKey = $this->_pathKey;
+ $options = compact('pathKey') + array('first' => true);
+ $value = $model::connection()->cast($this, array($name => $value), $options);
+ }
+ $this->_updated[$name] = $value;
+ unset($this->_increment[$name], $this->_removed[$name]);
+ }
+
+ protected function _setNested($name, $value) {
+ $current =& $this;
+ $path = explode('.', $name);
+ $length = count($path) - 1;
+
+ for ($i = 0; $i < $length; $i++) {
+ $key = $path[$i];
+
+ if (is_array($current) && isset($current[$key])) {
+ $next =& $current[$key];
+ } elseif (isset($current->{$key})) {
+ $next =& $current->{$key};
+ } else {
+ unset($next);
+ $next = null;
+ }
+
+ if ($next === null && ($model = $this->_model)) {
+ $current->__set($key, $model::connection()->item($model));
+ $next =& $current->{$key};
+ }
+ $current =& $next;
+ }
+
+ if (is_object($current)) {
+ $current->__set(end($path), $value);
+ }
+ }
+
+ /**
+ * PHP magic method used to check the presence of a field as document properties, i.e.
+ * `$document->_id`.
+ *
+ * @param $name The field name, as specified with an object property.
+ * @return boolean True if the field specified in `$name` exists, false otherwise.
+ */
+ public function __isset($name) {
+ $exists = isset($this->_data[$name]) || isset($this->_updated[$name]);
+ return ($exists && !isset($this->_removed[$name]));
+ }
+
+ /**
+ * PHP magic method used when unset() is called on a `Document` instance.
+ * Use case for this would be when you wish to edit a document and remove a field, ie.:
+ * {{{
+ * $doc = Post::find($id);
+ * unset($doc->fieldName);
+ * $doc->save();
+ * }}}
+ *
+ * @param string $name The name of the field to remove.
+ * @return void
+ */
+ public function __unset($name) {
+ $this->_removed[$name] = true;
+ unset($this->_updated[$name]);
+ }
+
+ /**
+ * Allows several properties to be assigned at once.
+ *
+ * For example:
+ * {{{
+ * $doc->set(array('title' => 'Lorem Ipsum', 'value' => 42));
+ * }}}
+ *
+ * @param $values An associative array of fields and values to assign to the `Document`.
+ * @return void
+ */
+ public function set($values) {
+ $this->__set($values);
+ }
+
+ /**
+ * Allows document fields to be accessed as array keys, i.e. `$document['_id']`.
+ *
+ * @param mixed $offset String or integer indicating the offset or index of a document in a set,
+ * or the name of a field in an individual document.
+ * @return mixed Returns either a sub-object in the document, or a scalar field value.
+ */
+ public function offsetGet($offset) {
+ return $this->__get($offset);
+ }
+
+ /**
+ * Allows document fields to be assigned as array keys, i.e. `$document['_id'] = $id`.
+ *
+ * @param mixed $offset String or integer indicating the offset or the name of a field in an
+ * individual document.
+ * @param mixed $value The value to assign to the field.
+ * @return void
+ */
+ public function offsetSet($offset, $value) {
+ return $this->__set(array($offset => $value));
+ }
+
+ /**
+ * Allows document fields to be tested as array keys, i.e. `isset($document['_id'])`.
+ *
+ * @param mixed $offset String or integer indicating the offset or the name of a field in an
+ * individual document.
+ * @param mixed $value The value to assign to the field.
+ * @return boolean Returns `true` if `$offset` is a field in the document, otherwise `false`.
+ */
+ public function offsetExists($offset) {
+ return $this->__isset($offset);
+ }
+
+ /**
+ * Allows document fields to be unset as array keys, i.e. `unset($document['_id'])`.
+ *
+ * @param mixed $offset String or integer indicating the offset or the name of a field in an
+ * individual document.
+ * @return void
+ */
+ public function offsetUnset($offset) {
+ return $this->__unset($offset);
+ }
+
+ /**
+ * Rewinds to the first item.
+ *
+ * @return mixed The current item after rewinding.
+ */
+ public function rewind() {
+ reset($this->_data);
+ $this->_valid = (count($this->_data) > 0);
+ return current($this->_data);
+ }
+
+ /**
+ * Used by the `Iterator` interface to determine the current state of the iteration, and when
+ * to stop iterating.
+ *
+ * @return boolean
+ */
+ public function valid() {
+ return $this->_valid;
+ }
+
+ public function current() {
+ return current($this->_data);
+ }
+
+ public function key() {
+ return key($this->_data);
+ }
+
+ /**
+ * Adds conversions checks to ensure certain class types and embedded values are properly cast.
+ *
+ * @param string $format Currently only `array` is supported.
+ * @param array $options
+ * @return mixed
+ */
+ public function to($format, array $options = array()) {
+ $defaults = array('handlers' => array(
+ 'MongoId' => function($value) { return (string) $value; },
+ 'MongoDate' => function($value) { return $value->sec; }
+ ));
+
+ if ($format == 'array') {
+ $options += $defaults;
+ $data = array_merge($this->_data, $this->_updated);
+ return Collection::toArray(array_diff_key($data, $this->_removed), $options);
+ }
+ return parent::to($format, $options);
+ }
+
+ /**
+ * Returns the next `Document` in the set, and advances the object's internal pointer. If the
+ * end of the set is reached, a new document will be fetched from the data source connection
+ * handle (`$_handle`). If no more records can be fetched, returns `null`.
+ *
+ * @return object|null Returns the next record in the set, or `null`, if no more records are
+ * available.
+ */
+ public function next() {
+ $prev = key($this->_data);
+ $this->_valid = (next($this->_data) !== false);
+ $cur = key($this->_data);
+
+ if (!$this->_valid && $cur !== $prev && $cur !== null) {
+ $this->_valid = true;
+ }
+ return $this->_valid ? $this->__get(key($this->_data)) : null;
+ }
+
+ /**
+ * Gets the raw data associated with this `Document`, or single item if `$field` is defined.
+ *
+ * @param string $name If included will only return the named item
+ * @return array Returns a raw array of `Document` data, or individual field value
+ */
+ public function data($name = null) {
+ if ($name) {
+ return parent::data($name);
+ }
+ $map = function($rel) { return $rel->data(); };
+ return $this->to('array') + array_map($map, $this->_relationships);
+ }
+
+ /**
+ * Safely (atomically) increments the value of the specified field by an arbitrary value.
+ * Defaults to `1` if no value is specified. Throws an exception if the specified field is
+ * non-numeric.
+ *
+ * @param string $field The name of the field to be incrememnted.
+ * @param string $value The value to increment the field by. Defaults to `1` if this parameter
+ * is not specified.
+ * @return integer Returns the current value of `$field`, based on the value retrieved from the
+ * data source when the entity was loaded, plus any increments applied. Note that it
+ * may not reflect the most current value in the persistent backend data source.
+ * @throws UnexpectedValueException Throws an exception when `$field` is set to a non-numeric
+ * type.
+ */
+ public function increment($field, $value = 1) {
+ if (!isset($this->_increment[$field])) {
+ $this->_increment[$field] = 0;
+ }
+ $this->_increment[$field] += $value;
+
+ if (!is_numeric($this->_data[$field])) {
+ throw new UnexpectedValueException("Field `{$field}` cannot be incremented.");
+ }
+ $this->_data[$field] += $value;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/entity/Record.php b/libraries/lithium/data/entity/Record.php
new file mode 100644
index 0000000..821598d
--- /dev/null
+++ b/libraries/lithium/data/entity/Record.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\entity;
+
+/**
+ * `Record` class. Represents data such as a row from a database. Records have fields (often known
+ * as columns in databases).
+ */
+class Record extends \lithium\data\Entity {
+
+ /**
+ * Overloading for reading inaccessible properties.
+ *
+ * @param string $name Property name.
+ * @return mixed Result.
+ */
+ public function &__get($name) {
+ $data = null;
+ $null = null;
+
+ if (isset($this->_relationships[$name])) {
+ return $this->_relationships[$name];
+ }
+
+ if (($model = $this->_model) && $this->_handle) {
+ foreach ($model::relations() as $relation => $config) {
+ $linkKey = $config->data('fieldName');
+ $type = $config->data('type') == 'hasMany' ? 'set' : 'entity';
+ $class = $this->_classes[$type];
+
+ if ($linkKey === $name) {
+ $data = isset($this->_data[$name]) ? $this->_data[$name] : array();
+ $this->_relationships[$name] = new $class();
+ return $this->_relationships[$name];
+ }
+ }
+ }
+ if (isset($this->_updated[$name])) {
+ return $this->_updated[$name];
+ }
+ if (isset($this->_data[$name])) {
+ return $this->_data[$name];
+ }
+ return $null;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/model/Query.php b/libraries/lithium/data/model/Query.php
index ed7afdd..3f695dc 100644
--- a/libraries/lithium/data/model/Query.php
+++ b/libraries/lithium/data/model/Query.php
@@ -2,14 +2,28 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data\model;
+use lithium\data\Source;
+use lithium\data\model\QueryException;
+
/**
- * Query class
+ * The `Query` class acts as a container for all information necessary to perform a particular
+ * database operation. Each `Query` object instance has a type, which is usually one of `'create'`,
+ * `'read'`, `'update'` or `'delete'`.
+ *
+ * Because of this, `Query` objects are the primary method of communication between `Model` classes
+ * and backend data sources. This helps to keep APIs abstract and flexible, since a model is only
+ * required to call a single method against its backend. Since the `Query` object simply acts as a
+ * structured data container, each backend can choose how to operate on the data the `Query`
+ * contains. See each class method for more details on what data this class supports.
+ *
+ * @see lithium\data\Model
+ * @see lithium\data\Source
*/
class Query extends \lithium\core\Object {
@@ -22,29 +36,68 @@ class Query extends \lithium\core\Object {
protected $_type = null;
/**
+ * Array containing mappings of relationship and field names, which allow database results to
+ * be mapped to the correct objects.
+ *
+ * @var array
+ */
+ protected $_map = array();
+
+ /**
* If a `Query` is bound to a `Record` or `Document` object (i.e. for a `'create'` or
* `'update'` query).
*
* @var object
*/
- protected $_binding = null;
+ protected $_entity = null;
+
+ /**
+ * An array of data used in a write context. Only used if no binding object is present in the
+ * `$_entity` property.
+ *
+ * @var array
+ */
+ protected $_data = array();
- protected $_autoConfig = array('type');
+ /**
+ * Auto configuration properties.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array('type', 'map');
+ /**
+ * Class constructor, which initializes the default values this object supports. Even though
+ * only a specific list of configuration parameters is available by default, the `Query` object
+ * uses the `__call()` method to implement automatic getters and setters for any arbitrary piece
+ * of data.
+ *
+ * This means that any information may be passed into the constructor may be used by the backend
+ * data source executing the query (or ignored, if support is not implemented). This is useful
+ * if, for example, you wish to extend a core data source and implement custom fucntionality.
+ *
+ * @param array $config
+ * @return void
+ */
public function __construct(array $config = array()) {
$defaults = array(
'calculate' => null,
'conditions' => array(),
'fields' => array(),
+ 'data' => array(),
'model' => null,
- 'table' => null,
+ 'alias' => null,
+ 'source' => null,
'order' => null,
+ 'offset' => null,
'limit' => null,
'page' => null,
- 'offset' => null,
'group' => null,
'comment' => null,
- 'joins' => array()
+ 'joins' => array(),
+ 'with' => array(),
+ 'map' => array(),
+ 'whitelist' => array(),
);
parent::__construct($config + $defaults);
}
@@ -54,12 +107,27 @@ class Query extends \lithium\core\Object {
unset($this->_config['type']);
foreach ($this->_config as $key => $val) {
- if (method_exists($this, $key)) {
+ if (method_exists($this, $key) && $val !== null) {
$this->_config[$key] = is_array($this->_config[$key]) ? array() : null;
$this->{$key}($val);
}
}
- unset($this->_config['record'], $this->_config['init']);
+ if ($list = $this->_config['whitelist']) {
+ $this->_config['whitelist'] = array_combine($list, $list);
+ }
+ if ($this->_config['with']) {
+ $this->_associate($this->_config['with']);
+ }
+ $joins = $this->_config['joins'];
+ $this->_config['joins'] = array();
+
+ foreach ($joins as $i => $join) {
+ $this->join($i, $join);
+ }
+ if ($this->_entity && !$this->_config['model']) {
+ $this->model($this->_entity->model());
+ }
+ unset($this->_config['entity'], $this->_config['init'], $this->_config['with']);
}
/**
@@ -71,16 +139,38 @@ class Query extends \lithium\core\Object {
return $this->_type;
}
+ /**
+ * Generates a schema map of the query's result set, where the keys are fully-namespaced model
+ * class names, and the values are arrays of field names.
+ *
+ * @param array $map
+ * @return array
+ */
+ public function map($map = null) {
+ if ($map !== null) {
+ $this->_map = $map;
+ return $this;
+ }
+ return $this->_map;
+ }
+
+ /**
+ * Accessor method for `Query` calculate values.
+ *
+ * @param string $calculate Value for calculate config setting.
+ * @return mixed Current calculate config value.
+ */
public function calculate($calculate = null) {
if ($calculate) {
$this->_config['calculate'] = $calculate;
+ return $this;
}
return $this->_config['calculate'];
}
/**
* Set and get method for the model associated with the `Query`.
- * Will also set the source table, i.e. `$this->_table`.
+ * Will also set the source table, i.e. `$this->_config['source']`.
*
* @param string $model
* @return string
@@ -88,7 +178,9 @@ class Query extends \lithium\core\Object {
public function model($model = null) {
if ($model) {
$this->_config['model'] = $model;
- $this->_config['table'] = $model::meta('source');
+ $this->_config['source'] = $model::meta('source');
+ $this->_config['name'] = $model::meta('name');
+ return $this;
}
return $this->_config['model'];
}
@@ -96,18 +188,19 @@ class Query extends \lithium\core\Object {
/**
* Set and get method for conditions.
*
- * If no conditions are set in query, it will ask the record for findById condition array.
+ * If no conditions are set in query, it will ask the bound entity for condition array.
*
- * @param array $conditions
- * @return array
+ * @param mixed $conditions String or array to append to existing conditions.
+ * @return array Returns an array of all conditions applied to this query.
*/
public function conditions($conditions = null) {
if ($conditions) {
$conditions = (array) $conditions;
$this->_config['conditions'] = (array) $this->_config['conditions'];
$this->_config['conditions'] = array_merge($this->_config['conditions'], $conditions);
+ return $this;
}
- return $this->_config['conditions'] ?: $this->_recordConditions();
+ return $this->_config['conditions'] ?: $this->_entityConditions();
}
/**
@@ -129,10 +222,11 @@ class Query extends \lithium\core\Object {
* }}}
*
* @param mixed $fields string, array or `false`
- * @return array|void
+ * @param boolean $overwrite If `true`, existing fields will be removed before adding `$fields`.
+ * @return array Returns an array containing all fields added to the query.
*/
- public function fields($fields = null) {
- if ($fields === false) {
+ public function fields($fields = null, $overwrite = false) {
+ if ($fields === false || $overwrite) {
$this->_config['fields'] = array();
}
$this->_config['fields'] = (array) $this->_config['fields'];
@@ -142,6 +236,9 @@ class Query extends \lithium\core\Object {
} elseif ($fields && !isset($this->_config['fields'][$fields])) {
$this->_config['fields'][] = $fields;
}
+ if ($fields !== null) {
+ return $this;
+ }
return $this->_config['fields'];
}
@@ -154,6 +251,7 @@ class Query extends \lithium\core\Object {
public function limit($limit = null) {
if ($limit) {
$this->_config['limit'] = intval($limit);
+ return $this;
}
return $this->_config['limit'];
}
@@ -165,8 +263,9 @@ class Query extends \lithium\core\Object {
* @return integer
*/
public function offset($offset = null) {
- if ($offset) {
+ if ($offset !== null) {
$this->_config['offset'] = intval($offset);
+ return $this;
}
return $this->_config['offset'];
}
@@ -179,14 +278,15 @@ class Query extends \lithium\core\Object {
*/
public function page($page = null) {
if ($page) {
- $this->_config['page'] = intval($page) ?: 1;
- $this->offset(($this->_config['page'] - 1) * $this->_config['limit']);
+ $this->_config['page'] = $page = (intval($page) ?: 1);
+ $this->offset(($page - 1) * $this->_config['limit']);
+ return $this;
}
return $this->_config['page'];
}
/**
- * Set and get method for the query's order specification
+ * Set and get method for the query's order specification.
*
* @param array|string $order
* @return mixed
@@ -194,13 +294,21 @@ class Query extends \lithium\core\Object {
public function order($order = null) {
if ($order) {
$this->_config['order'] = $order;
+ return $this;
}
return $this->_config['order'];
}
+ /**
+ * Set and get method for the `Query` group config setting.
+ *
+ * @param string $group New group config setting.
+ * @return mixed Current group config setting.
+ */
public function group($group = null) {
if ($group) {
$this->_config['group'] = $group;
+ return $this;
}
return $this->_config['group'];
}
@@ -216,21 +324,23 @@ class Query extends \lithium\core\Object {
public function comment($comment = null) {
if ($comment) {
$this->_config['comment'] = $comment;
+ return $this;
}
return $this->_config['comment'];
}
/**
- * Set and get method for the query's record instance
+ * Set and get method for the query's entity instance.
*
- * @param object $binding reference to the query's current record
- * @return object reference to the query's current record
+ * @param object $entity Reference to the query's current entity object.
+ * @return object Reference to the query's current entity object.
*/
- public function &record(&$binding = null) {
- if ($binding) {
- $this->_binding = $binding;
+ public function &entity(&$entity = null) {
+ if ($entity) {
+ $this->_entity = $entity;
+ return $this;
}
- return $this->_binding;
+ return $this->_entity;
}
/**
@@ -240,35 +350,54 @@ class Query extends \lithium\core\Object {
* @return array Empty array if no data, array of data if the record has it.
*/
public function data($data = array()) {
+ $bind =& $this->_entity;
+
if ($data) {
- return $this->_binding ? $this->_binding->set($data) : null;
+ $bind ? $bind->set($data) : $this->_data = array_merge($this->_data, $data);
+ return $this;
}
- return $this->_binding ? $this->_binding->data() : array();
+ $data = $bind ? $bind->data() : $this->_data;
+ return ($list = $this->_config['whitelist']) ? array_intersect_key($data, $list) : $data;
}
/**
* Set and get the join queries
*
- * @param query|array $joins a single query object or an array of query objects
+ * @param string $name Optional name of join. Unless two parameters are passed, this parameter
+ * is regonized as `$join`.
+ * @param object|string $join A single query object or an array of query objects
* @return array of query objects
*/
- public function join($joins = null) {
- if ($joins) {
- $this->_config['joins'] = array_merge($this->_config['joins'], (array) $joins);
+ public function join($name = null, $join = null) {
+ if (is_scalar($name) && !$join && isset($this->_config['joins'][$name])) {
+ return $this->_config['joins'][$name];
+ }
+ if ($name && !$join) {
+ $join = $name;
+ $name = null;
+ }
+ if ($join) {
+ $join = is_array($join) ? $this->_instance(get_class($this), $join) : $join;
+ $name ? $this->_config['joins'][$name] = $join : $this->_config['joins'][] = $join;
+ return $this;
}
return $this->_config['joins'];
}
/**
- * Convert the query's properties to the data-sources' syntax and return it as an array.
+ * Convert the query's properties to the data sources' syntax and return it as an array.
*
- * @param object $dataSource Instance of the data-source to use for conversion.
- * @return array Converted properties.
+ * @param object $dataSource Instance of the data source to use for conversion.
+ * @param array $options Options to use when exporting the data.
+ * @return array Returns an array containing a data source-specific representation of a query.
*/
- public function export($dataSource) {
- $keys = array_keys($this->_config);
+ public function export(Source $dataSource, array $options = array()) {
+ $defaults = array('keys' => array());
+ $options += $defaults;
+
+ $keys = $options['keys'] ?: array_keys($this->_config);
$methods = $dataSource->methods();
- $results = array();
+ $results = array('type' => $this->_type);
$apply = array_intersect($keys, $methods);
$copy = array_diff($keys, $apply);
@@ -277,13 +406,85 @@ class Query extends \lithium\core\Object {
$results[$item] = $dataSource->{$item}($this->{$item}(), $this);
}
foreach ($copy as $item) {
- $results[$item] = $this->_config[$item];
+ if (in_array($item, $keys)) {
+ $results[$item] = $this->_config[$item];
+ }
+ }
+ if (in_array('data', $keys)) {
+ $results['data'] = $this->_exportData();
+ }
+ if (isset($results['source'])) {
+ $results['source'] = $dataSource->name($results['source']);
+ }
+ if (!isset($results['fields'])) {
+ return $results;
+ }
+ $created = array('fields', 'values');
+
+ if (is_array($results['fields']) && array_keys($results['fields']) == $created) {
+ $results = $results['fields'] + $results;
}
- $results['table'] = $dataSource->name($this->_config['table']);
return $results;
}
/**
+ * Helper method used by `export()` to extract the data either from a bound entity, or from
+ * passed configuration, and filter it through a configured whitelist, if present.
+ *
+ * @return array
+ */
+ protected function _exportData() {
+ $data = $this->_entity ? $this->_entity->export() : $this->_data;
+
+ if (!$list = $this->_config['whitelist']) {
+ return $data;
+ }
+ $list = array_combine($list, $list);
+
+ if (!$this->_entity) {
+ return array_intersect_key($data, $list);
+ }
+ foreach ($data as $type => $values) {
+ if (!is_array($values)) {
+ continue;
+ }
+ $data[$type] = array_intersect_key($values, $list);
+ }
+ return $data;
+ }
+
+ public function schema($field = null) {
+ if (is_array($field)) {
+ $this->_config['schema'] = $field;
+ return $this;
+ }
+
+ if (isset($this->_config['schema'])) {
+ $schema = $this->_config['schema'];
+
+ if ($field) {
+ return isset($schema[$field]) ? $schema[$field] : null;
+ }
+ return $schema;
+ }
+
+ if ($model = $this->model()) {
+ return $model::schema($field);
+ }
+ }
+
+ public function alias($alias = null) {
+ if ($alias) {
+ $this->_config['alias'] = $alias;
+ return $this;
+ }
+ if (!$this->_config['alias'] && ($model = $this->_config['model'])) {
+ $this->_config['alias'] = $model::meta('name');
+ }
+ return $this->_config['alias'];
+ }
+
+ /**
* Gets a custom query field which does not have an accessor method.
*
* @param string $method Query part.
@@ -293,6 +494,7 @@ class Query extends \lithium\core\Object {
public function __call($method, $params = array()) {
if ($params) {
$this->_config[$method] = current($params);
+ return $this;
}
return isset($this->_config[$method]) ? $this->_config[$method] : null;
}
@@ -301,22 +503,37 @@ class Query extends \lithium\core\Object {
* Will return a find first condition on the associated model if a record is connected.
* Called by conditions when it is called as a get and no condition is set.
*
- * @return array ([model's primary key'] => [that key set in the record]).
+ * @return array Returns an array in the following format:
+ * `([model's primary key'] => [that key set in the record])`.
*/
- protected function _recordConditions() {
- if (!$this->_binding) {
+ protected function _entityConditions() {
+ if (!$this->_entity || !($model = $this->_config['model'])) {
return;
}
- $model = $this->_config['model'];
-
- if (!$model) {
- return null;
- }
- if (is_array($key = $model::key($this->_binding))) {
+ if (is_array($key = $model::key($this->_entity->data()))) {
return $key;
}
$key = $model::meta('key');
- return array($key => $this->_binding->{$key});
+ $val = $this->_entity->{$key};
+ return $val ? array($key => $val) : array();
+ }
+
+ protected function _associate($related) {
+ if (!$model = $this->model()) {
+ return;
+ }
+ $queryClass = get_class($this);
+
+ foreach ((array) $related as $name => $config) {
+ if (is_int($name)) {
+ $name = $config;
+ $config = array();
+ }
+ if (!$relation = $model::relations($name)) {
+ throw new QueryException("Related model not found.");
+ }
+ $config += $relation->data();
+ }
}
}
diff --git a/libraries/lithium/data/model/QueryException.php b/libraries/lithium/data/model/QueryException.php
new file mode 100644
index 0000000..b284542
--- /dev/null
+++ b/libraries/lithium/data/model/QueryException.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\model;
+
+/**
+ * The `MediaException` is thrown when a request is made to render content in a format not
+ * supported.
+ *
+ * @see lithium\net\http\Media
+ */
+class QueryException extends \RuntimeException {
+
+ protected $code = 500;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/model/Record.php b/libraries/lithium/data/model/Record.php
deleted file mode 100644
index 50e3696..0000000
--- a/libraries/lithium/data/model/Record.php
+++ /dev/null
@@ -1,204 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\data\model;
-
-/**
- * `Record` class. Represents data such as a row from a database. Records have fields (often known
- * as columns in databases).
- */
-class Record extends \lithium\core\Object {
-
- /**
- * Namespaced name of model that this record is linked to.
- */
- protected $_model = null;
-
- /**
- * Associative array of the records fields with values
- */
- protected $_data = array();
-
- /**
- * Validation errors
- */
- protected $_errors = array();
-
- /**
- * An array of flags to track which fields in this record have been modified, where the keys
- * are field names, and the values are always true. If, for example, a change to a field is
- * reverted, that field's flag should be unset from the list.
- *
- * @var array
- */
- protected $_modified = array();
-
- /**
- * A flag indicating whether or not this record exists. Set to false if this is a newly-created
- * record, or if this record has been loaded and subsequently deleted. True if the record has
- * been loaded from the database, or has been created and subsequently saved.
- *
- * @var boolean
- */
- protected $_exists = false;
-
- protected $_autoConfig = array('model', 'exists', 'data' => 'merge');
-
- protected $_hasValidated = false;
-
- /**
- * Creates a new record object with default values.
- *
- * Options defined:
- * - 'data' _array_: Data to enter into the record. Defaults to an empty array.
- * - 'model' _string_: Class name that provides the data-source for this record.
- * Defaults to `null`.
- *
- * @param array $config
- * @return object Record object.
- */
- public function __construct(array $config = array()) {
- $defaults = array('model' => null, 'data' => array());
- parent::__construct($config + $defaults);
- }
-
- public function __get($name) {
- return isset($this->_data[$name]) ? $this->_data[$name] : null;
- }
-
- public function __set($name, $value) {
- $this->_modified[$name] = true;
- $this->_data[$name] = $value;
- }
-
- public function __isset($name) {
- return array_key_exists($name, $this->_data);
- }
-
- /**
- * Allows several properties to be assigned at once, i.e.:
- * {{{
- * $record->set(array('title' => 'Lorem Ipsum', 'value' => 42));
- * }}}
- *
- * @param $values An associative array of fields and values to assign to the `Record`.
- * @return void
- */
- public function set($values) {
- foreach ($values as $name => $value) {
- $this->__set($name, $value);
- }
- }
-
- /**
- * Access the data fields of the record. Can also access a $named field.
- *
- * @param string $name Optionally included field name.
- * @return array|string Entire data array if $name is empty, otherwise the value from the named
- * field.
- */
- public function data($name = null) {
- return empty($name) ? $this->_data : $this->__get($name);
- }
-
- /**
- * Access the errors of the record.
- *
- * @param array|string $field If an array, overwrites `$this->_errors`. If a string, and $value
- * is not null, sets the corresponding key in $this->_errors to $value
- * @param string $value Value to set.
- * @return array|string Either the $this->_errors array, or single value from it.
- */
- public function errors($field = null, $value = null) {
- if ($field === null) {
- return $this->_errors;
- }
- if (is_array($field)) {
- $this->_errors = $field;
- return $this->_errors;
- }
- if ($value === null && isset($this->_errors[$field])) {
- return $this->_errors[$field];
- }
- if ($value !== null) {
- return $this->_errors[$field] = $value;
- }
- return $value;
- }
-
- /**
- * Magic method that allows calling of model methods on this record instance, i.e.:
- * {{{
- * $record->validates();
- * }}}
- *
- * @param string $method
- * @param array $params
- * @return mixed
- */
- public function __call($method, $params) {
- $model = $this->_model;
-
- if (!$model) {
- return null;
- }
- array_unshift($params, $this);
- $class = $model::invokeMethod('_instance');
- return call_user_func_array(array(&$class, $method), $params);
- }
-
- /**
- * A flag indicating whether or not this record exists.
- *
- * @return boolean `True` if the record was `read` from the data-source, or has been `create`d
- * and `save`d. Otherwise `false`.
- */
- public function exists() {
- return $this->_exists;
- }
-
- /**
- * Called after a `Record` is saved. Updates the object's internal state to reflect the
- * corresponding database record, and sets the `Record`'s primary key, if this is a
- * newly-created object.
- *
- * @param $id The ID to assign, where applicable.
- * @return void
- */
- public function update($id = null) {
- if ($id) {
- $id = (array) $id;
- $model = $this->_model;
- foreach ((array) $model::meta('key') as $i => $key) {
- $this->__set($key, $id[$i]);
- }
- }
- $this->_exists = true;
- }
-
- /**
- * Converts the data in the record set to a different format, i.e. an array.
- *
- * @param string $format currently only `array`
- * @param array $options
- * @return mixed
- */
- public function to($format, array $options = array()) {
- switch ($format) {
- case 'array':
- $result = $this->_data;
- break;
- default:
- $result = $this;
- break;
- }
- return $result;
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/data/model/Relationship.php b/libraries/lithium/data/model/Relationship.php
new file mode 100644
index 0000000..f615834
--- /dev/null
+++ b/libraries/lithium/data/model/Relationship.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\model;
+
+use lithium\core\Libraries;
+use lithium\util\Inflector;
+use lithium\core\ClassNotFoundException;
+
+/**
+ * The `Relationship` class encapsulates the data and functionality necessary to link two model
+ * classes together.
+ */
+class Relationship extends \lithium\core\Object {
+
+ /**
+ * A relationship linking type defined by one document or record (or multiple) being embedded
+ * within another.
+ */
+ const LINK_EMBEDDED = 'embedded';
+
+ /**
+ * The reciprocal of `LINK_EMBEDDED`, this defines a linking type wherein an embedded document
+ * references the document that contains it.
+ */
+ const LINK_CONTAINED = 'contained';
+
+ /**
+ * A one-to-one or many-to-one relationship in which a key contains an ID value linking to
+ * another document or record.
+ */
+ const LINK_KEY = 'key';
+
+ /**
+ * A many-to-many relationship in which a key contains an embedded array of IDs linking to other
+ * records or documents.
+ */
+ const LINK_KEY_LIST = 'keylist';
+
+ /**
+ * A relationship defined by a database-native reference mechanism, linking a key to an
+ * arbitrary record or document in another data collection or entirely separate database.
+ */
+ const LINK_REF = 'ref';
+
+ public function __construct(array $config = array()) {
+ $defaults = array(
+ 'name' => null,
+ 'keys' => array(),
+ 'type' => null,
+ 'to' => null,
+ 'from' => null,
+ 'link' => self::LINK_KEY,
+ 'fields' => true,
+ 'fieldName' => null,
+ 'constraint' => array(),
+ );
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+ $config = $this->_config;
+ $singularName = $config['name'];
+
+ if ($config['type'] == 'hasMany') {
+ $singularName = Inflector::singularize($config['name']);
+ }
+ if (!$config['to']) {
+ $assoc = preg_replace("/\\w+$/", "", $config['from']) . $singularName;
+ $config['to'] = class_exists($assoc) ? $assoc : Libraries::locate('models', $assoc);
+ }
+ if (!$config['fieldName']) {
+ $config['fieldName'] = lcfirst($config['name']);
+ }
+ $config['keys'] = $this->_keys($config['keys'], $config);
+ $this->_config = $config;
+ }
+
+ public function data($key = null) {
+ if (!$key) {
+ return $this->_config;
+ }
+ return isset($this->_config[$key]) ? $this->_config[$key] : null;
+ }
+
+ public function __get($name) {
+ return $this->data($name);
+ }
+
+ protected function _keys($keys, $config) {
+ if (!($related = ($config['type'] == 'belongsTo') ? $config['to'] : $config['from'])) {
+ return array();
+ }
+ if (class_exists($related)) {
+ return array_combine((array) $keys, (array) $related::key());
+ }
+ throw new ClassNotFoundException("Related model class '{$related}' not found.");
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/Database.php b/libraries/lithium/data/source/Database.php
index 84e7bb6..ff65f6a 100644
--- a/libraries/lithium/data/source/Database.php
+++ b/libraries/lithium/data/source/Database.php
@@ -2,15 +2,25 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data\source;
-use \lithium\util\String;
-use \InvalidArgumentException;
+use lithium\util\String;
+use lithium\core\Libraries;
+use lithium\util\Inflector;
+use InvalidArgumentException;
+/**
+ * The `Database` class provides the base-level abstraction for SQL-oriented relational databases.
+ * It handles all aspects of abstraction, including formatting for basic query types and SQL
+ * fragments (i.e. for joins), converting `Query` objects to SQL, and various other functionality
+ * which is shared across multiple relational databases.
+ *
+ * @see lithium\data\model\Query
+ */
abstract class Database extends \lithium\data\Source {
/**
@@ -25,45 +35,67 @@ abstract class Database extends \lithium\data\Source {
/**
* Strings used to render the given statement
*
- * @see \lithium\data\source\Database::renderCommand()
- * @var string
+ * @see lithium\data\source\Database::renderCommand()
+ * @var array
*/
protected $_strings = array(
- 'read' => "
- SELECT {:fields} From {:table} {:joins} {:conditions} {:group} {:order} {:limit};
- {:comment}
- ",
- 'create' => "INSERT INTO {:table} ({:fields}) VALUES ({:values});{:comment}",
- 'update' => "UPDATE {:table} SET {:fields} {:conditions};{:comment}",
- 'delete' => "DELETE {:flags} From {:table} {:aliases} {:conditions};{:comment}",
- 'schema' => "CREATE TABLE {:table} (\n{:columns}{:indexes});{:comment}",
- 'join' => "{:type} JOIN {:table} ON {:constraint}"
+ 'create' => "INSERT INTO {:source} ({:fields}) VALUES ({:values});{:comment}",
+ 'update' => "UPDATE {:source} SET {:fields} {:conditions};{:comment}",
+ 'delete' => "DELETE {:flags} FROM {:source} {:alias} {:conditions};{:comment}",
+ 'schema' => "CREATE TABLE {:source} (\n{:columns}{:indexes});{:comment}",
+ 'join' => "{:type} JOIN {:source} {:alias} {:constraint}"
);
+ /**
+ * Classes used by `Database`.
+ *
+ * @var array
+ */
protected $_classes = array(
- 'record' => '\lithium\data\model\Record',
- 'recordSet' => '\lithium\data\collection\RecordSet'
+ 'entity' => 'lithium\data\entity\Record',
+ 'set' => 'lithium\data\collection\RecordSet',
+ 'relationship' => 'lithium\data\model\Relationship'
);
/**
- * Getter/Setter for the connection's encoding
- * Abstract. Must be defined by child class.
+ * List of SQL operators, paired with handling options.
*
- * @param mixed $encoding
- * @return mixed.
+ * @var array
*/
- abstract public function encoding($encoding = null);
+ protected $_operators = array(
+ '=' => array('multiple' => 'IN'),
+ '<' => array(),
+ '>' => array(),
+ '<=' => array(),
+ '>=' => array(),
+ '!=' => array('multiple' => 'NOT IN'),
+ '<>' => array('multiple' => 'NOT IN'),
+ 'between' => array('format' => 'BETWEEN ? AND ?'),
+ 'BETWEEN' => array('format' => 'BETWEEN ? AND ?'),
+ 'like' => array(),
+ 'LIKE' => array()
+ );
+
+ protected $_constraintTypes = array(
+ 'AND' => true,
+ 'OR' => true
+ );
/**
- * Handle the result return from the
+ * A pair of opening/closing quote characters used for quoting identifiers in SQL queries.
+ *
+ * @var array
+ */
+ protected $_quotes = array();
+
+ /**
+ * Getter/Setter for the connection's encoding
* Abstract. Must be defined by child class.
*
- * @param string $type next|close The current step in the iteration.
- * @param mixed $resource The result resource returned from the database.
- * @param \lithium\data\model\Query $context The given query.
- * @return void
+ * @param mixed $encoding
+ * @return mixed.
*/
- abstract public function result($type, $resource, $context);
+ abstract public function encoding($encoding = null);
/**
* Return the last errors produced by a the execution of a query.
@@ -76,7 +108,7 @@ abstract class Database extends \lithium\data\Source {
* Execute a given query
* Abstract. Must be defined by child class.
*
- * @see \lithium\data\source\Database::renderCommand()
+ * @see lithium\data\source\Database::renderCommand()
* @param string $sql The sql string to execute
* @return resource
*/
@@ -86,7 +118,7 @@ abstract class Database extends \lithium\data\Source {
* Get the last insert id from the database.
* Abstract. Must be defined by child class.
*
- * @param \lithium\data\model\Query $context The given query.
+ * @param lithium\data\model\Query $context The given query.
* @return void
*/
abstract protected function _insertId($query);
@@ -95,10 +127,10 @@ abstract class Database extends \lithium\data\Source {
* Creates the database object and set default values for it.
*
* Options defined:
- * - 'database' _string_ Name of the database to use. Defaults to 'lithium'.
+ * - 'database' _string_ Name of the database to use. Defaults to `null`.
* - 'host' _string_ Name/address of server to connect to. Defaults to 'localhost'.
* - 'login' _string_ Username to use when connecting to server. Defaults to 'root'.
- * - 'password' _string_ Password to use when connecting to server. Defaults to none.
+ * - 'password' _string_ Password to use when connecting to server. Defaults to `''`.
* - 'persistent' _boolean_ If true a persistent connection will be attempted, provided the
* adapter supports it. Defaults to `true`.
*
@@ -113,6 +145,10 @@ abstract class Database extends \lithium\data\Source {
'password' => '',
'database' => null,
);
+ $this->_strings += array(
+ 'read' => 'SELECT {:fields} FROM {:source} {:alias} {:joins} {:conditions} {:group} ' .
+ '{:order} {:limit};{:comment}'
+ );
parent::__construct($config + $defaults);
}
@@ -123,21 +159,27 @@ abstract class Database extends \lithium\data\Source {
* @return string
*/
public function name($name) {
- return $name;
+ $open = reset($this->_quotes);
+ $close = next($this->_quotes);
+ if (preg_match('/^[a-z0-9_-]+\.[a-z0-9_-]+$/i', $name)) {
+ list($first, $second) = explode('.', $name, 2);
+ return "{$open}{$first}{$close}.{$open}{$second}{$close}";
+ }
+ return preg_match('/^[a-z0-9_-]+$/i', $name) ? "{$open}{$name}{$close}" : $name;
}
/**
* Converts a given value into the proper type based on a given schema definition.
*
- * @see \lithium\data\source\Database::schema()
+ * @see lithium\data\source\Database::schema()
* @param mixed $value The value to be converted. Arrays will be recursively converted.
- * @param array $schema Formatted array from `\lithium\data\source\Database::schema()`
+ * @param array $schema Formatted array from `lithium\data\source\Database::schema()`
* @return mixed value with converted type
*/
public function value($value, array $schema = array()) {
if (is_array($value)) {
foreach ($value as $key => $val) {
- $value[$key] = $this->value($val, $schema);
+ $value[$key] = $this->value($val, isset($schema[$key]) ? $schema[$key] : $schema);
}
return $value;
}
@@ -146,93 +188,100 @@ abstract class Database extends \lithium\data\Source {
}
switch ($type = isset($schema['type']) ? $schema['type'] : $this->_introspectType($value)) {
case 'boolean':
- return $this->_toBoolean($value);
+ return $this->_toNativeBoolean($value);
case 'float':
return floatval($value);
case 'integer':
return intval($value);
}
- return "'{$value}'";
}
/**
* Inserts a new record into the database based on a the `Query`. The record is updated
* with the id of the insert.
*
- * @param object $query A `\lithium\data\model\Query` object
- * @param array $options none
- * @return boolean
+ * @see lithium\util\String::insert()
+ * @param object $query An SQL query string, or `lithium\data\model\Query` object instance.
+ * @param array $options If $query is a string, $options contains an array of bind values to be
+ * escaped, quoted, and inserted into `$query` using `String::insert()`.
+ * @return boolean Returns `true` if the query succeeded, otherwise `false`.
* @filter
*/
public function create($query, array $options = array()) {
return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
$query = $params['query'];
- $model = $query->model();
- $fields = $values = array();
- $data = $query->export($self);
- $schema = (array) $model::schema();
-
- while (list($field, $value) = each($data['fields'])) {
- $schema += array($field => array('default' => null));
- if ($value === null && $schema[$field]['default'] === null) {
- continue;
- }
- $fields[] = $self->name($field);
- $values[] = $self->value($value, $schema[$field]);
+ $model = $entity = $object = $id = null;
+
+ if (is_object($query)) {
+ $object = $query;
+ $model = $query->model();
+ $params = $query->export($self);
+ $entity =& $query->entity();
+ $query = $self->renderCommand('create', $params, $query);
+ } else {
+ $query = String::insert($query, $self->value($params['options']));
+ }
+
+ if (!$self->invokeMethod('_execute', array($query))) {
+ return false;
}
- $fields = join(', ', $fields);
- $values = join(', ', $values);
- $sql = $self->renderCommand('create', compact('fields', 'values') + $data, $query);
- if ($self->invokeMethod('_execute', array($sql))) {
- $id = null;
- if (!$model::key($query->record())) {
- $id = $self->invokeMethod('_insertId', array($query));
+ if ($entity) {
+ if (($model) && !$model::key($entity)) {
+ $id = $self->invokeMethod('_insertId', array($object));
}
- $query->record()->update($id);
- return true;
+ $entity->update($id);
}
- return false;
+ return true;
});
}
/**
- * Reads records from a database using a `\lithium\data\model\Query` object or raw SQL string.
+ * Reads records from a database using a `lithium\data\model\Query` object or raw SQL string.
*
- * @param string|object $query `\lithium\data\model\Query` object or sql string
- * @param string $options
- * - `return` : switch return between `'array'`, `'item'`, or `'resource'`.
- * default: `item`. Requires a `Query` object
- * @return mixed Determined by `$options['return'].
+ * @param string|object $query `lithium\data\model\Query` object or SQL string.
+ * @param string $options If `$query` is a raw string, contains the values that will be escaped
+ * and quoted. Other options:
+ * - `'return'` _string_: switch return between `'array'`, `'item'`, or
+ * `'resource'` _string_: Defaults to `'item'`.
+ * @return mixed Determined by `$options['return']`.
* @filter
*/
public function read($query, array $options = array()) {
- $defaults = array('return' => 'item');
+ $defaults = array('return' => is_string($query) ? 'array' : 'item', 'schema' => array());
$options += $defaults;
return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
$query = $params['query'];
- $options = $params['options'];
-
- $sql = is_string($query) ? $query : $self->renderCommand($query);
+ $args = $params['options'];
+ $return = $args['return'];
+ unset($args['return']);
+
+ if (is_string($query)) {
+ $sql = String::insert($query, $self->value($args));
+ } else {
+ $sql = $self->renderCommand($query);
+ }
$result = $self->invokeMethod('_execute', array($sql));
- switch ($options['return']) {
+ switch ($return) {
case 'resource':
return $result;
case 'array':
- $columns = $self->schema($query, $result);
+ $columns = $args['schema'] ?: $self->schema($query, $result);
$records = array();
- while ($data = $self->result('next', $result, null)) {
+ while ($data = $result->next()) {
+ // @hack: Fix this to support relationships
+ if (count($columns) != count($data) && is_array(current($columns))) {
+ $columns = current($columns);
+ }
$records[] = array_combine($columns, $data);
}
- $self->result('close', $result, null);
return $records;
case 'item':
return $self->item($query->model(), array(), compact('query', 'result') + array(
- 'class' => 'recordSet',
- 'handle' => $self,
+ 'class' => 'set',
));
}
});
@@ -241,27 +290,20 @@ abstract class Database extends \lithium\data\Source {
/**
* Updates a record in the database based on the given `Query`.
*
- * @param object $query A `\lithium\data\model\Query` object
+ * @param object $query A `lithium\data\model\Query` object
* @param array $options none
* @return boolean
*/
public function update($query, array $options = array()) {
return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
$query = $params['query'];
- $model = $query->model();
- $data = $query->export($self);
- $schema = (array) $model::schema();
- $fields = array();
-
- while (list($field, $value) = each($data['fields'])) {
- $schema += array($field => array());
- $fields[] = $self->name($field) . ' = ' . $self->value($value, $schema[$field]);
- }
- $fields = join(', ', $fields);
- $sql = $self->renderCommand('update', compact('fields') + $data, $query);
+ $params = $query->export($self);
+ $sql = $self->renderCommand('update', $params, $query);
if ($self->invokeMethod('_execute', array($sql))) {
- $query->record()->update();
+ if ($query->entity()) {
+ $query->entity()->update();
+ }
return true;
}
return false;
@@ -271,44 +313,73 @@ abstract class Database extends \lithium\data\Source {
/**
* Deletes a record in the database based on the given `Query`.
*
- * @param object $query A `\lithium\data\model\Query` object
- * @param array $options none
- * @return boolean
+ * @param object $query An SQL string, or `lithium\data\model\Query` object instance.
+ * @param array $options If `$query` is a string, `$options` is the array of quoted/escaped
+ * parameter values to be inserted into the query.
+ * @return boolean Returns `true` on successful query execution (not necessarily if records are
+ * deleted), otherwise `false`.
*/
public function delete($query, array $options = array()) {
return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
- extract($params);
- $data = $query->export($self);
+ $query = $params['query'];
- if (!$data['conditions']) {
- return false;
+ if (is_object($query)) {
+ $data = $query->export($self);
+ $sql = $self->renderCommand('delete', $data, $query);
+ } else {
+ $sql = String::insert($query, $self->value($params['options']));
}
- $sql = $self->renderCommand('delete', $data, $query);
return (boolean) $self->invokeMethod('_execute', array($sql));
});
}
/**
- * Returns a newly-created `Record` object, bound to a model and populated with default data
- * and options.
+ * Executes calculation-related queries, such as those required for `count` and other
+ * aggregates.
*
- * @param string $model A fully-namespaced class name representing the model class to which the
- * `Record` object will be bound.
- * @param array $data The default data with which the new `Record` should be populated.
- * @param array $options Any additional options to pass to the `Record`'s constructor.
- * @return object Returns a new, un-saved `Record` object bound to the model class specified in
- * `$model`.
+ * @param string $type Only accepts `count`.
+ * @param mixed $query The query to be executed.
+ * @param array $options Optional arguments for the `read()` query that will be executed
+ * to obtain the calculation result.
+ * @return integer Result of the calculation.
*/
- public function item($model, array $data = array(), array $options = array()) {
- $class = $this->_classes[isset($options['class']) ? $options['class'] : 'record'];
- return new $class(compact('model', 'data') + $options);
+ public function calculation($type, $query, array $options = array()) {
+ $query->calculate($type);
+
+ switch ($type) {
+ case 'count':
+ if (strpos($fields = $this->fields($query->fields(), $query), ',') !== false) {
+ $fields = "*";
+ }
+ $query->fields("COUNT({$fields}) as count", true);
+ $query->map(array($query->model() => array('count')));
+ list($record) = $this->read($query, $options)->data();
+ return isset($record['count']) ? intval($record['count']) : null;
+ }
}
/**
- * Returns a given `type` statement for the given data, rendered from the Database::$_strings
+ * Defines or modifies the default settings of a relationship between two models.
*
- * @param string $type create|read|update|delete|join
- * @param string $data The data to replace in the string
+ * @param string $class
+ * @param string $type
+ * @param string $name
+ * @param array $config
+ * @return array Returns an array containing the configuration for a model relationship.
+ */
+ public function relationship($class, $type, $name, array $config = array()) {
+ $singularName = ($type == 'hasMany') ? Inflector::singularize($name) : $name;
+ $keys = $type == 'belongsTo' ? $class::meta('name') : $singularName;
+ $keys = Inflector::underscore($keys) . '_id';
+ $from = $class;
+ return $this->_instance('relationship', $config + compact('type', 'name', 'keys', 'from'));
+ }
+
+ /**
+ * Returns a given `type` statement for the given data, rendered from `Database::$_strings`.
+ *
+ * @param string $type One of `'create'`, `'read'`, `'update'`, `'delete'` or `'join'`.
+ * @param string $data The data to replace in the string.
* @param string $context
* @return string
*/
@@ -319,7 +390,7 @@ abstract class Database extends \lithium\data\Source {
$type = $context->type();
}
if (!isset($this->_strings[$type])) {
- throw new InvalidArgumentException("Invalid query type '{$type}'");
+ throw new InvalidArgumentException("Invalid query type `{$type}`.");
}
$data = array_filter($data);
return trim(String::insert($this->_strings[$type], $data, array('clean' => true)));
@@ -329,7 +400,7 @@ abstract class Database extends \lithium\data\Source {
* Builds an array of keyed on the fully-namespaced `Model` with array of fields as values
* for the given `Query`
*
- * @param object $query A `\lithium\data\model\Query` object
+ * @param object $query A `lithium\data\model\Query` object
* @param string $resource
* @param string $context
* @return void
@@ -337,28 +408,31 @@ abstract class Database extends \lithium\data\Source {
public function schema($query, $resource = null, $context = null) {
$model = $query->model();
$fields = $query->fields();
- $relations = $model::relations();
$result = array();
- $ns = function($class) use ($model) {
- static $namespace;
- $namespace = $namespace ?: preg_replace('/\w+$/', '', $model);
- return "{$namespace}{$class}";
- };
+ if (!$model && is_array($fields)) {
+ return array($fields);
+ }
- if (empty($fields)) {
+ if (!$fields) {
return array($model => array_keys($model::schema()));
}
+ $namespace = preg_replace('/\w+$/', '', $model);
+ $relations = $model ? $model::relations() : array();
+ $schema = $model::schema();
foreach ($fields as $scope => $field) {
switch (true) {
case (is_numeric($scope) && $field == '*'):
$result[$model] = array_keys($model::schema());
break;
- case (is_numeric($scope) && in_array($field, $relations)):
+ case (is_numeric($scope) && isset($schema[$field])):
+ $result[$model][] = $field;
+ break;
+ case (is_numeric($scope) && isset($relations[$field])):
$scope = $field;
case (in_array($scope, $relations, true) && $field == '*'):
- $scope = $ns($scope);
+ $scope = $namespace . $scope;
$result[$scope] = array_keys($scope::schema());
break;
case (in_array($scope, $relations)):
@@ -370,16 +444,24 @@ abstract class Database extends \lithium\data\Source {
}
/**
- * Returns a string of formatted conditions to be inserted into the query statement
+ * Returns a string of formatted conditions to be inserted into the query statement. If the
+ * query conditions are defined as an array, key pairs are converted to SQL strings.
+ *
+ * Conversion rules are as follows:
+ *
+ * - If `$key` is numeric and `$value` is a string, `$value` is treated as a literal SQL
+ * fragment and returned.
*
* @param string|array $conditions The conditions for this query.
- * @param object $context The current `\lithium\data\model\Query`.
+ * @param object $context The current `lithium\data\model\Query` instance.
* @param array $options
- * - `prepend` : added before WHERE clause
- * @return void
+ * - `prepend` _boolean_: Whether the return string should be prepended with the
+ * `WHERE` keyword.
+ * @return string Returns the `WHERE` clause of an SQL query.
*/
public function conditions($conditions, $context, array $options = array()) {
$defaults = array('prepend' => true);
+ $ops = $this->_operators;
$options += $defaults;
$model = $context->model();
$schema = $model ? $model::schema() : array();
@@ -390,65 +472,103 @@ abstract class Database extends \lithium\data\Source {
case is_string($conditions):
return ($options['prepend']) ? "WHERE {$conditions}" : $conditions;
case !is_array($conditions):
- return null;
+ return '';
}
-
$result = array();
- $boolean = 'AND';
foreach ($conditions as $key => $value) {
$schema[$key] = isset($schema[$key]) ? $schema[$key] : array();
+ $return = $this->_processConditions($key,$value, $schema);
- switch (true) {
- case (is_numeric($key) && is_string($value)):
- $result[] = $value;
- break;
- case (is_string($key) && is_object($value)):
- $value = trim(rtrim($this->renderCommand($value), ';'));
- $result[] = "{$key} IN ({$value})";
- break;
- case (is_string($key) && is_array($value)):
- $value = join(', ', $this->value($value, $schema[$key]));
- $result[] = "{$key} IN ({$value})";
- break;
- default:
- $value = $this->value($value, $schema[$key]);
- $result[] = "{$key} = {$value}";
- break;
+ if ($return) {
+ $result[] = $return;
}
}
- $result = join(" {$boolean} ", $result);
+ $result = join(" AND ", $result);
return ($options['prepend'] && !empty($result)) ? "WHERE {$result}" : $result;
}
+ protected function _processConditions($key, $value, $schema, $glue = 'AND'){
+ $constraintTypes = &$this->_constraintTypes;
+
+ switch (true) {
+ case (is_numeric($key) && is_string($value)):
+ return $value;
+ case is_string($value):
+ return $this->name($key) . ' = ' . $this->value($value);
+ case is_numeric($key) && is_array($value):
+ $result = array();
+ foreach($value as $cField => $cValue) {
+ $result[] = $this->_processConditions($cField, $cValue, $schema, $glue);
+ }
+ return '(' . implode(' ' . $glue . ' ', $result) . ')';
+ case (is_string($key) && is_object($value)):
+ $value = trim(rtrim($this->renderCommand($value), ';'));
+ return "{$key} IN ({$value})";
+ case is_array($value) && isset($constraintTypes[strtoupper($key)]):
+ $result = array();
+ $glue = strtoupper($key);
+
+ foreach($value as $cField => $cValue) {
+ $result[] = $this->_processConditions($cField, $cValue, $schema, $glue);
+ }
+ return '(' . implode(' ' . $glue . ' ', $result) . ')';
+ case (is_string($key) && is_array($value) && isset($this->_operators[key($value)])):
+ foreach ($value as $op => $val) {
+ $result[] = $this->_operator($key, array($op => $val), $schema[$key]);
+ }
+ return '(' . implode(' ' . $glue . ' ', $result) . ')';
+ case is_array($value):
+ $value = join(', ', $this->value($value, $schema));
+ return "{$key} IN ({$value})";
+ default:
+ if (isset($value)) {
+ $value = $this->value($value, $schema);
+ return "{$key} = {$value}";
+ }
+ break;
+ }
+ return false;
+ }
+
/**
- * Returns
+ * Returns either a formatted string for a select query, or an array of key/value pairs for a
+ * create or update query.
*
- * @param string $fields
- * @param string $context
- * @return void
+ * @param array $fields Either an array of field names for a select, or key/value pairs for
+ * a create or update query.
+ * @param string $context An instance of `Query`, containing the details of the query to be run.
+ * @return mixed Returns a string or array, depending on the query type to be performed (as
+ * determined by `$context->type()`).
*/
public function fields($fields, $context) {
- switch ($context->type()) {
- case 'create':
- case 'update':
- return $fields ?: $context->data();
- default:
- return empty($fields) ? '*' : join(', ', $fields);
+ $type = $context->type();
+ $model = $context->model();
+ $schema = $model ? (array) $model::schema() : array();
+
+ if ($type == 'create' || $type == 'update') {
+ $data = $context->data();
+
+ if ($fields && is_array($fields) && is_int(key($fields))) {
+ $data = array_intersect_key($data, array_combine($fields, $fields));
+ }
+ $method = "_{$type}Fields";
+ return $this->{$method}($data, $schema, $context);
}
+ return empty($fields) ? '*' : join(', ', $fields);
}
/**
* Returns a LIMIT statement from the given limit and the offset of the context object.
*
* @param integer $limit An
- * @param object $context The `\lithium\data\model\Query` object
+ * @param object $context The `lithium\data\model\Query` object
* @return string
*/
public function limit($limit, $context) {
- if (empty($limit)) {
+ if (!$limit) {
return;
- };
+ }
if ($offset = $context->offset() ?: '') {
$offset .= ', ';
}
@@ -458,81 +578,77 @@ abstract class Database extends \lithium\data\Source {
/**
* Returns a join statement for given array of query objects
*
- * @param object|array $joins A single or array of `\lithium\data\model\Query` objects
- * @param object $context The parent `\lithium\data\model\Query` object
+ * @param object|array $joins A single or array of `lithium\data\model\Query` objects
+ * @param object $context The parent `lithium\data\model\Query` object
* @return string
*/
- public function joins($joins, $context) {
+ public function joins(array $joins, $context) {
$result = null;
- foreach ((array) $joins as $join) {
+ foreach ($joins as $join) {
$result .= $this->renderCommand('join', $join->export($this));
}
return $result;
}
- public function order($order, $context) {
- $direction = 'DESC';
-
- if (is_string($order) && strpos($order, ',') && !preg_match('/\(.+\,.+\)/', $order)) {
- $order = array_map('trim', explode(',', $order));
+ public function constraint($constraint, $context) {
+ if (!$constraint) {
+ return "";
}
- $order = (is_array($order) ? array_filter($order) : $order);
-
- if (empty($order)) {
- return '';
+ if (is_string($constraint)) {
+ return "ON {$constraint}";
}
+ $result = array();
- if (is_array($order)) {
- $keys = (Set::countDim($keys) > 1) ? array_map(array(&$this, 'order'), $keys) : $keys;
+ foreach ($constraint as $field => $value) {
+ if (is_string($value)) {
+ $result[] = $this->name($field) . ' = ' . $this->name($value);
+ }
+ }
+ return 'ON ' . join(' AND ', $result);
+ }
- foreach ($keys as $key => $value) {
- if (is_numeric($key)) {
- $key = $value = ltrim(str_replace('ORDER BY ', '', $this->order($value)));
- $value = (!preg_match('/\\x20ASC|\\x20DESC/i', $key) ? ' ' . $direction : '');
- } else {
- $value = ' ' . $value;
- }
+ /**
+ * Return formatted clause for order.
+ *
+ * @param mixed $order The `order` clause to be formatted
+ * @param object $context
+ * @return mixed Formatted `order` clause.
+ */
+ public function order($order, $context) {
+ $direction = 'ASC';
+ $model = $context->model();
- if (!preg_match('/^.+\\(.*\\)/', $key) && !strpos($key, ',')) {
- if (preg_match('/\\x20ASC|\\x20DESC/i', $key, $dir)) {
- $dir = $dir[0];
- $key = preg_replace('/\\x20ASC|\\x20DESC/i', '', $key);
- } else {
- $dir = '';
- }
- $key = trim($key);
- if (!preg_match('/\s/', $key)) {
- $key = $this->name($key);
- }
- $key .= ' ' . trim($dir);
- }
- $order[] = $this->order($key . $value);
+ if (is_string($order)) {
+ if (!$model::schema($order)) {
+ $match = '/\s+(A|DE)SC/i';
+ return "ORDER BY {$order}" . (preg_match($match, $order) ? '' : " {$direction}");
}
- return ' ORDER BY ' . trim(str_replace('ORDER BY', '', join(',', $order)));
+ $order = array($order => $direction);
}
- $keys = preg_replace('/ORDER\\x20BY/i', '', $order);
- if (strpos($order, '.')) {
- preg_match_all(
- '/([a-zA-Z0-9_]{1,})\\.([a-zA-Z0-9_]{1,})/', $keys, $result, PREG_PATTERN_ORDER
- );
- $pregCount = count($result[0]);
+ if (!is_array($order)) {
+ return;
+ }
+ $result = array();
- for ($i = 0; $i < $pregCount; $i++) {
- if (!is_numeric($result[0][$i])) {
- $keys = preg_replace(
- '/' . $result[0][$i] . '/', $this->name($result[0][$i]), $keys
- );
- }
+ foreach ($order as $column => $dir) {
+ if (is_int($column)) {
+ $column = $dir;
+ $dir = $direction;
}
- $result = ' ORDER BY ' . $keys;
- return $result . (!preg_match('/\\x20ASC|\\x20DESC/i', $keys) ? ' ' . $direction : '');
+ $dir = in_array($dir, array('ASC', 'asc', 'DESC', 'desc')) ? $dir : $direction;
- } elseif (preg_match('/(\\x20ASC|\\x20DESC)/i', $keys, $match)) {
- $direction = $match[1];
- return ' ORDER BY ' . preg_replace('/' . $match[1] . '/', '', $keys) . $direction;
+ if (!$model) {
+ $result[] = "{$column} {$dir}";
+ continue;
+ }
+ if ($field = $model::schema($column)) {
+ $name = $this->name($model::meta('name')) . '.' . $this->name($column);
+ $result[] = "{$name} {$dir}";
+ }
}
- return ' ORDER BY ' . $keys . ' ' . $direction;
+ $order = join(', ', $result);
+ return "ORDER BY {$order}";
}
/**
@@ -541,7 +657,83 @@ abstract class Database extends \lithium\data\Source {
* @param string $comment
* @return string
*/
- public function comment($comment) {}
+ public function comment($comment) {
+ return $comment ? "/* {$comment} */" : null;
+ }
+
+ public function alias($alias, $context) {
+ if (!$alias && ($model = $context->model())) {
+ $alias = $model::meta('name');
+ }
+ return $alias ? "AS " . $this->name($alias) : null;
+ }
+
+ public function cast($entity, array $data, array $options = array()) {
+ return $data;
+ }
+
+ protected function _createFields($data, $schema, $context) {
+ $fields = $values = array();
+
+ while (list($field, $value) = each($data)) {
+ $fields[] = $this->name($field);
+ $values[] = $this->value($value, isset($schema[$field]) ? $schema[$field] : array());
+ }
+ $fields = join(', ', $fields);
+ $values = join(', ', $values);
+ return compact('fields', 'values');
+ }
+
+ protected function _updateFields($data, $schema, $context) {
+ $fields = array();
+
+ while (list($field, $value) = each($data)) {
+ $schema += array($field => array('default' => null));
+
+ if ($value === null && $schema[$field]['default'] === null) {
+ continue;
+ }
+ $fields[] = $this->name($field) . ' = ' . $this->value($value, $schema[$field]);
+ }
+ return join(', ', $fields);
+ }
+
+ /**
+ * Handles conversion of SQL operator keys to SQL statements.
+ *
+ * @param string $key Key in a conditions array. Usually a field name.
+ * @param mixed $value An SQL operator or comparison value.
+ * @param array $schema An array defining the schema of the field used in the criteria.
+ * @param array $options
+ * @return string Returns an SQL string representing part of a `WHERE` clause of a query.
+ */
+ protected function _operator($key, $value, array $schema = array(), array $options = array()) {
+ $defaults = array('boolean' => 'AND');
+ $options += $defaults;
+
+ list($op, $value) = each($value);
+ $config = $this->_operators[$op];
+ $key = $this->name($key);
+ $values = array();
+
+ foreach ((array) $value as $val) {
+ $values[] = $this->value($val, $schema);
+ }
+
+ switch (true) {
+ case (isset($config['format'])):
+ return $key . ' ' . String::insert($config['format'], $values);
+ case (count($values) > 1 && isset($config['multiple'])):
+ $op = $config['multiple'];
+ $values = join(', ', $values);
+ return "{$key} {$op} ({$values})";
+ case (count($values) > 1):
+ return join(" {$options['boolean']} ", array_map(
+ function($v) use ($key, $op) { return "{$key} {$op} {$v}"; }, $values
+ ));
+ }
+ return "{$key} {$op} {$values[0]}";
+ }
/**
* Returns a fully-qualified table name (i.e. with prefix), quoted.
@@ -599,6 +791,10 @@ abstract class Database extends \lithium\data\Source {
}
return (boolean) $value;
}
+
+ protected function _toNativeBoolean($value) {
+ return $value ? 1 : 0;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/Http.php b/libraries/lithium/data/source/Http.php
index 09e8bfc..7454a23 100644
--- a/libraries/lithium/data/source/Http.php
+++ b/libraries/lithium/data/source/Http.php
@@ -2,19 +2,33 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data\source;
-use \lithium\core\Libraries;
+use lithium\core\Libraries;
+use lithium\util\String;
/**
- * Http class to access data sources using \lithium\net\http\Service.
+ * Http class to access data sources using `lithium\net\http\Service`.
*/
class Http extends \lithium\data\Source {
+ /**
+ * Service connection
+ *
+ * @var object lithium\net\http\Service
+ */
+ public $connection = null;
+
+ /**
+ * The set of array keys which will be auto-populated in the object's protected properties from
+ * constructor parameters.
+ *
+ * @var array
+ */
protected $_autoConfig = array('classes' => 'merge');
/**
@@ -23,22 +37,28 @@ class Http extends \lithium\data\Source {
* @var array
*/
protected $_classes = array(
- 'service' => '\lithium\net\http\Service'
+ 'service' => 'lithium\net\http\Service',
+ 'relationship' => 'lithium\data\model\Relationship'
);
/**
- * Service connection
+ * Is Connected?
*
- * @var object lithium\net\http\Service
+ * @var boolean
*/
- protected $_connection = null;
+ protected $_isConnected = false;
/**
- * Is Connected?
+ * List of methods and their corresponding HTTP method and path.
*
- * @var boolean
+ * @var array
*/
- protected $_isConnected = false;
+ protected $_methods = array(
+ 'read' => array('method' => 'get', 'path' => "/{:source}"),
+ 'create' => array('method' => 'post', 'path' => "/{:source}"),
+ 'update' => array('method' => 'put', 'path' => "/{:source}/{:id}"),
+ 'delete' => array('method' => 'delete', 'path' => "/{:source}/{:id}")
+ );
/**
* Constructor
@@ -48,32 +68,27 @@ class Http extends \lithium\data\Source {
*/
public function __construct(array $config = array()) {
$defaults = array(
- 'classes' => array(),
- 'adapter' => null,
+ 'adapter' => null,
'persistent' => false,
- 'protocol' => 'tcp',
+ 'scheme' => 'http',
'host' => 'localhost',
'version' => '1.1',
- 'auth' => 'Basic',
- 'login' => 'root',
+ 'auth' => null,
+ 'login' => '',
'password' => '',
'port' => 80,
- 'timeout' => 1,
- 'encoding' => 'UTF-8'
+ 'timeout' => 30,
+ 'encoding' => 'UTF-8',
);
- $config = (array) $config + $defaults;
-
- $config['auth'] = array(
- 'method' => $config['auth'],
- 'username' => $config['login'],
- 'password' => $config['password']
- );
- $this->_classes = $config['classes'] + $this->_classes;
+ $config = $config + $defaults;
+ $config['username'] = $config['login'];
parent::__construct($config);
}
protected function _init() {
- $this->_connection = new $this->_classes['service']($this->_config);
+ $config = $this->_config;
+ unset($config['type']);
+ $this->connection = $this->_instance('service', $config);
parent::_init();
}
@@ -84,7 +99,7 @@ class Http extends \lithium\data\Source {
* @return mixed
*/
public function __get($property) {
- return $this->_connection->{$property};
+ return $this->connection->{$property};
}
/**
@@ -95,7 +110,23 @@ class Http extends \lithium\data\Source {
* @return mixed
*/
public function __call($method, $params) {
- return $this->_connection->invokeMethod($method, $params);
+ if (isset($this->_config['methods'][$method])) {
+ $params += array(array(), array());
+ $string = $this->_config['methods'][$method];
+
+ if (!isset($string['path'])) {
+ $string['path'] = $method;
+ }
+ $conn =& $this->connection;
+ $filter = function($self, $params) use (&$conn, $string) {
+ $data = in_array($string['method'], array('post', 'put'))
+ ? (array) $params[0] : array();
+ $path = String::insert($string['path'], $data, array('clean' => true));
+ return $conn->{$string['method']}($path, $data, $params[1]);
+ };
+ return $this->_filter(__METHOD__, $params, $filter);
+ }
+ return $this->connection->invokeMethod($method, $params);
}
/**
@@ -104,7 +135,7 @@ class Http extends \lithium\data\Source {
* @return boolean
*/
public function connect() {
- if (!$this->_isConnected && $this->_connection->connect()) {
+ if (!$this->_isConnected) {
$this->_isConnected = true;
}
return $this->_isConnected;
@@ -116,10 +147,8 @@ class Http extends \lithium\data\Source {
* @return boolean
*/
public function disconnect() {
- if ($this->_isConnected) {
- if ($this->_connection->disconnect()) {
- $this->_isConnected = false;
- }
+ if ($this->_isConnected && $this->connection !== null) {
+ $this->_isConnected = false;
}
return !$this->_isConnected;
}
@@ -141,7 +170,8 @@ class Http extends \lithium\data\Source {
* @param string $meta
* @return void
*/
- public function describe($entity, $meta = array()) {
+ public function describe($entity, array $meta = array()) {
+ return array();
}
/**
@@ -152,7 +182,28 @@ class Http extends \lithium\data\Source {
* @return void
*/
public function create($query, array $options = array()) {
- return $this->_connection->post();
+ $params = compact('query', 'options');
+ $config = $this->_config;
+
+ if (!isset($this->_methods[__FUNCTION__])) {
+ return null;
+ }
+ $method = $this->_methods[__FUNCTION__];
+ $filter = function($self, $params) use ($config, $method) {
+ $query = $params['query'];
+ $options = $params['options'];
+ $data = array();
+
+ if ($query) {
+ $options += array_filter($query->export($self), function($v) {
+ return $v !== null;
+ });
+ $data = $query->data();
+ }
+ $path = String::insert($method['path'], $options, array('clean' => true));
+ return $self->connection->{$method['method']}($path, $data, $options);
+ };
+ return $this->_filter(__METHOD__, $params, $filter);
}
/**
@@ -163,7 +214,30 @@ class Http extends \lithium\data\Source {
* @return string
*/
public function read($query, array $options = array()) {
- return $this->_connection->get();
+ $params = compact('query', 'options');
+ $conn =& $this->connection;
+
+ if (!isset($this->_methods[__FUNCTION__])) {
+ return null;
+ }
+ $method = $this->_methods[__FUNCTION__];
+ $filter = function($self, $params) use (&$conn, $method) {
+ $query = $params['query'];
+ $options = $params['options'];
+ $data = array();
+ $defaults = array('conditions' => null, 'limit' => null);
+
+ if ($query) {
+ $options += array_filter($query->export($self), function($v) {
+ return $v !== null;
+ });
+ $options += $defaults;
+ $data = (array) $options['conditions'] + (array) $options['limit'];
+ }
+ $path = String::insert($method['path'], $options, array('clean' => true));
+ return $conn->{$method['method']}($path, $data, $options);
+ };
+ return $this->_filter(__METHOD__, $params, $filter);
}
/**
@@ -174,7 +248,28 @@ class Http extends \lithium\data\Source {
* @return string
*/
public function update($query, array $options = array()) {
- return $this->_connection->put();
+ $params = compact('query', 'options');
+ $conn =& $this->connection;
+
+ if (!isset($this->_methods[__FUNCTION__])) {
+ return null;
+ }
+ $method = $this->_methods[__FUNCTION__];
+ $filter = function($self, $params) use (&$conn, $method) {
+ $query = $params['query'];
+ $options = $params['options'];
+ $data = array();
+
+ if ($query) {
+ $options += array_filter($query->export($self), function($v) {
+ return $v !== null;
+ });
+ $data = $query->data();
+ }
+ $path = String::insert($method['path'], $options + $data, array('clean' => true));
+ return $conn->{$method['method']}($path, $data, $options);
+ };
+ return $this->_filter(__METHOD__, $params, $filter);
}
/**
@@ -184,8 +279,48 @@ class Http extends \lithium\data\Source {
* @param array $options
* @return string
*/
- public function delete($query = null, array $options = array()) {
- return $this->_connection->delete();
+ public function delete($query, array $options = array()) {
+ $params = compact('query', 'options');
+ $conn =& $this->connection;
+
+ if (!isset($this->_methods[__FUNCTION__])) {
+ return null;
+ }
+ $method = $this->_methods[__FUNCTION__];
+ $filter = function($self, $params) use (&$conn, $method) {
+ $query = $params['query'];
+ $options = $params['options'];
+ $data = array();
+
+ if ($query) {
+ $options += $query->export($self);
+ $data = $query->data();
+ }
+ $path = String::insert($method['path'], $options + $data, array('clean' => true));
+ return $conn->{$method['method']}($path, array(), $options);
+ };
+ return $this->_filter(__METHOD__, $params, $filter);
+ }
+
+ /**
+ * Defines or modifies the default settings of a relationship between two models.
+ *
+ * @param string $class
+ * @param string $type
+ * @param string $name
+ * @param array $options
+ * @return array Returns an array containing the configuration for a model relationship.
+ */
+ public function relationship($class, $type, $name, array $options = array()) {
+ if (isset($this->_classes['relationship'])) {
+ $class = $this->_classes['relationship'];
+ return ($class) ? new $class() : null;
+ }
+ return null;
+ }
+
+ public function name($name) {
+ return $name;
}
}
diff --git a/libraries/lithium/data/source/Mock.php b/libraries/lithium/data/source/Mock.php
new file mode 100644
index 0000000..970fd59
--- /dev/null
+++ b/libraries/lithium/data/source/Mock.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source;
+
+/**
+ * The `Mock` data source is used behind-the-scenes when a model does not use a backend data source.
+ * It implements the necessary methods, but does not support querying and has no storage backend.
+ * It can create generic entities for use in forms and elsewhere within the framework. This allows
+ * developers to create domain objects with business logic and schemas, without worrying about
+ * backend storage.
+ */
+class Mock extends \lithium\data\Source {
+
+ public function connect() {
+ return true;
+ }
+
+ public function disconnect() {
+ return true;
+ }
+
+ public function entities($class = null) {
+ return array();
+ }
+
+ public function describe($entity, array $meta = array()) {
+ return array();
+ }
+
+ public function relationship($class, $type, $name, array $options = array()) {
+ return false;
+ }
+
+ public function create($query, array $options = array()) {
+ return false;
+ }
+
+ public function read($query, array $options = array()) {
+ return false;
+ }
+
+ public function update($query, array $options = array()) {
+ return false;
+ }
+
+ public function delete($query, array $options = array()) {
+ return false;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/MongoDb.php b/libraries/lithium/data/source/MongoDb.php
index d2fbbea..3077f42 100644
--- a/libraries/lithium/data/source/MongoDb.php
+++ b/libraries/lithium/data/source/MongoDb.php
@@ -2,17 +2,23 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data\source;
-use \Mongo;
-use \MongoId;
-use \MongoCode;
-use \MongoDBRef;
-use \Exception;
+use Mongo;
+use MongoId;
+use MongoCode;
+use MongoDate;
+use MongoDBRef;
+use MongoRegex;
+use MongoBinData;
+use MongoGridFSFile;
+use lithium\util\Inflector;
+use lithium\core\NetworkException;
+use Exception;
/**
* A data source adapter which allows you to connect to the MongoDB database engine. MongoDB is an
@@ -25,50 +31,157 @@ use \Exception;
* (i.e. arrays) including other `Document` objects.
*
* After installing MongoDB, you can connect to it as follows:
- * {{{//app/config/connections.php:
+ * {{{//app/config/bootstrap/connections.php:
* Connections::add('default', array('type' => 'MongoDb', 'database' => 'myDb'));}}}
*
* By default, it will attempt to connect to a Mongo instance running on `localhost` on port
* 27017. See `__construct()` for details on the accepted configuration settings.
*
- * @see lithium\data\collection\Document
+ * @see lithium\data\entity\Document
* @see lithium\data\Connections::add()
* @see lithium\data\source\MongoDb::__construct()
*/
class MongoDb extends \lithium\data\Source {
- protected $_db = null;
+ /**
+ * The Mongo class instance.
+ *
+ * @var object
+ */
+ public $server = null;
+ /**
+ * The MongoDB object instance.
+ *
+ * @var object
+ */
+ public $connection = null;
+
+ /**
+ * Classes used by this class.
+ *
+ * @var array
+ */
protected $_classes = array(
- 'document' => '\lithium\data\collection\Document'
+ 'entity' => 'lithium\data\entity\Document',
+ 'array' => 'lithium\data\collection\DocumentArray',
+ 'set' => 'lithium\data\collection\DocumentSet',
+ 'result' => 'lithium\data\source\mongo_db\Result',
+ 'exporter' => 'lithium\data\source\mongo_db\Exporter',
+ 'relationship' => 'lithium\data\model\Relationship',
+ );
+
+ /**
+ * Map of typical SQL-like operators to their MongoDB equivalents.
+ *
+ * @var array Keys are SQL-like operators, value is the MongoDB equivalent.
+ */
+ protected $_operators = array(
+ '<' => '$lt',
+ '>' => '$gt',
+ '<=' => '$lte',
+ '>=' => '$gte',
+ '!=' => array('single' => '$ne', 'multiple' => '$nin'),
+ '<>' => array('single' => '$ne', 'multiple' => '$nin'),
+ 'or' => '$or',
+ '||' => '$or',
+ 'not' => '$not',
+ '!' => '$not',
);
/**
+ * A closure or anonymous function which receives an instance of this class, a collection name
+ * and associated meta information, and returns an array defining the schema for an associated
+ * model, where the keys are field names, and the values are arrays defining the type
+ * information for each field. At a minimum, type arrays must contain a `'type'` key. For more
+ * information on schema definitions, and an example schema callback implementation, see the
+ * `$_schema` property of the `Model` class.
+ *
+ * @see lithium\data\Model::$_schema
+ * @var Closure
+ */
+ protected $_schema = null;
+
+ /**
+ * An array of closures that handle casting values to specific types.
+ *
+ * @var array
+ */
+ protected $_handlers = array();
+
+ /**
+ * List of configuration keys which will be automatically assigned to their corresponding
+ * protected class properties.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array('schema', 'handlers', 'classes' => 'merge');
+
+ /**
* Instantiates the MongoDB adapter with the default connection information.
*
+ * @see lithium\data\Connections::add()
+ * @see lithium\data\source\MongoDb::$_schema
* @param array $config All information required to connect to the database, including:
- * - `'database'`: The name of the database to connect to. Defaults to 'lithium'.
- * - `'host'`: The IP or machine name where Mongo is running. Defaults to 'localhost'.
- * - `'persistent'`: If a persistent connection (if available) should be made. Defaults
- * to true.
- * - `'port'`: The port number Mongo is listening on. The default is '27017'.
+ * - `'database'` _string_: The name of the database to connect to. Defaults to `null`.
+ * - `'host'` _string_: The IP or machine name where Mongo is running, followed by a
+ * colon, and the port number. Defaults to `'localhost:27017'`.
+ * - `'persistent'` _mixed_: Determines a persistent connection to attach to. See the
+ * `$options` parameter of
+ * [`Mongo::__construct()`](http://www.php.net/manual/en/mongo.construct.php) for
+ * more information. Defaults to `false`, meaning no persistent connection is made.
+ * - `'timeout'` _integer_: The number of milliseconds a connection attempt will wait
+ * before timing out and throwing an exception. Defaults to `100`.
+ * - `'schema'` _closure_: A closure or anonymous function which returns the schema
+ * information for a model class. See the `$_schema` property for more information.
+ * - `'gridPrefix'` _string_: The default prefix for MongoDB's `chunks` and `files`
+ * collections. Defaults to `'fs'`.
+ * - `'replicaSet'` _boolean_: See the documentation for `Mongo::__construct()`. Defaults
+ * to `false`.
*
* Typically, these parameters are set in `Connections::add()`, when adding the adapter to the
* list of active connections.
* @return object The adapter instance.
- *
- * @see lithium\data\Connections::add()
*/
public function __construct(array $config = array()) {
$defaults = array(
- 'persistent' => true,
- 'host' => 'localhost',
- 'database' => 'lithium',
- 'port' => '27017',
+ 'persistent' => false,
+ 'login' => null,
+ 'password' => null,
+ 'host' => Mongo::DEFAULT_HOST . ':' . Mongo::DEFAULT_PORT,
+ 'database' => null,
+ 'timeout' => 100,
+ 'replicaSet' => false,
+ 'schema' => null,
+ 'gridPrefix' => 'fs',
);
parent::__construct($config + $defaults);
}
+ protected function _init() {
+ parent::_init();
+
+ $this->_operators += array(
+ 'like' => function($key, $value) { return new MongoRegex($value); }
+ );
+
+ $this->_handlers += array(
+ 'id' => function($v) {
+ return is_string($v) && preg_match('/^[0-9a-f]{24}$/', $v) ? new MongoId($v) : $v;
+ },
+ 'date' => function($v) {
+ $v = is_numeric($v) ? intval($v) : strtotime($v);
+ return (!$v || time() == $v) ? new MongoDate() : new MongoDate($v);
+ },
+ 'regex' => function($v) { return new MongoRegex($v); },
+ 'integer' => function($v) { return (integer) $v; },
+ 'float' => function($v) { return (float) $v; },
+ 'boolean' => function($v) { return (boolean) $v; },
+ 'code' => function($v) { return new MongoCode($v); },
+ 'binary' => function($v) { return new MongoBinData($v); },
+ );
+ }
+
/**
* Ensures that the server connection is closed and resources are freed when the adapter
* instance is destroyed.
@@ -82,49 +195,117 @@ class MongoDb extends \lithium\data\Source {
}
/**
- * Configures a model class by overriding the default dependencies for `'recordSet'` and
- * `'record'` , and sets the primary key to `'_id'`, in keeping with Mongo's conventions.
+ * With no parameter, checks to see if the `mongo` extension is installed. With a parameter,
+ * queries for a specific supported feature.
*
+ * @param string $feature Test for support for a specific feature, i.e. `"transactions"` or
+ * `"arrays"`.
+ * @return boolean Returns `true` if the particular feature (or if MongoDB) support is enabled,
+ * otherwise `false`.
+ */
+ public static function enabled($feature = null) {
+ if (!$feature) {
+ return extension_loaded('mongo');
+ }
+ $features = array(
+ 'arrays' => true,
+ 'transactions' => false,
+ 'booleans' => true,
+ 'relationships' => true,
+ );
+ return isset($features[$feature]) ? $features[$feature] : null;
+ }
+
+ /**
+ * Configures a model class by overriding the default dependencies for `'set'` and
+ * `'entity'` , and sets the primary key to `'_id'`, in keeping with Mongo's conventions.
+ *
+ * @see lithium\data\Model::$_meta
+ * @see lithium\data\Model::$_classes
* @param string $class The fully-namespaced model class name to be configured.
* @return Returns an array containing keys `'classes'` and `'meta'`, which will be merged with
* their respective properties in `Model`.
- * @see lithium\data\Model::$_meta
- * @see lithium\data\Model::$_classes
*/
public function configureClass($class) {
- return array('meta' => array('key' => '_id'), 'classes' => array());
+ return array(
+ 'meta' => array('key' => '_id', 'locked' => false),
+ 'schema' => array()
+ );
}
+ /**
+ * Connects to the Mongo server.
+ *
+ * @return boolean Returns `true` the connection attempt was successful, otherwise `false`.
+ */
public function connect() {
- $config = $this->_config;
+ $cfg = $this->_config;
$this->_isConnected = false;
- $this->_connection = new Mongo("mongodb://{$config['host']}:{$config['port']}", array(
- 'persist' => $config['persistent']
- ));
- if ($this->_db = $this->_connection->{$config['database']}) {
- $this->_isConnected = true;
+ $host = is_array($cfg['host']) ? join(',', $cfg['host']) : $cfg['host'];
+ $login = $cfg['login'] ? "{$cfg['login']}:{$cfg['password']}@" : '';
+ $connection = "mongodb://{$login}{$host}" . ($login ? "/{$cfg['database']}" : '');
+ $options = array(
+ 'connect' => true, 'timeout' => $cfg['timeout'], 'replicaSet' => $cfg['replicaSet']
+ );
+
+ try {
+ if ($persist = $cfg['persistent']) {
+ $options['persist'] = $persist === true ? 'default' : $persist;
+ }
+ $this->server = new Mongo($connection, $options);
+
+ if ($this->connection = $this->server->{$cfg['database']}) {
+ $this->_isConnected = true;
+ }
+ } catch (Exception $e) {
+ throw new NetworkException("Could not connect to the database.", 503, $e);
}
return $this->_isConnected;
}
+ /**
+ * Disconnect from the Mongo server.
+ *
+ * @return boolean True on successful disconnect, false otherwise.
+ */
public function disconnect() {
- if ($this->_connection && $this->_connection->connected) {
+ if ($this->server && $this->server->connected) {
try {
- $this->_isConnected = !$this->_connection->close();
+ $this->_isConnected = !$this->server->close();
} catch (Exception $e) {}
- unset($this->_db, $this->_connection);
+ unset($this->connection, $this->server);
return !$this->_isConnected;
}
return true;
}
+ /**
+ * Returns the list of collections in the currently-connected database.
+ *
+ * @param string $class The fully-name-spaced class name of the model object making the request.
+ * @return array Returns an array of objects to which models can connect.
+ */
public function entities($class = null) {
- return array_map(function($col) { return $col->getName(); }, $this->_db->listCollections());
+ $this->_checkConnection();
+ $conn = $this->connection;
+ return array_map(function($col) { return $col->getName(); }, $conn->listCollections());
}
- public function describe($entity, $meta = array()) {
- return array();
+ /**
+ * Gets the column 'schema' for a given MongoDB collection. Only returns a schema if the
+ * `'schema'` configuration flag has been set in the constructor.
+ *
+ * @see lithium\data\source\MongoDb::$_schema
+ * @param mixed $entity Would normally specify a collection name.
+ * @param array $meta
+ * @return array Returns an associative array describing the given collection's schema.
+ */
+ public function describe($entity, array $meta = array()) {
+ if (!$schema = $this->_schema) {
+ return array();
+ }
+ return $schema($this, $entity, $meta);
}
/**
@@ -154,135 +335,229 @@ class MongoDb extends \lithium\data\Source {
* @return mixed The return value of the native method specified in `$method`.
*/
public function __call($method, $params) {
- return call_user_func_array(array(&$this->_connection, $method), $params);
+ if ((!$this->server) && !$this->connect()) {
+ return null;
+ }
+ return call_user_func_array(array(&$this->server, $method), $params);
}
+ /**
+ * Normally used in cases where the query is a raw string (as opposed to a `Query` object),
+ * to database must determine the correct column names from the result resource. Not
+ * applicable to this data source.
+ *
+ * @param mixed $query
+ * @param resource $resource
+ * @param object $context
+ * @return array
+ */
public function schema($query, $resource = null, $context = null) {
return array();
}
+ /**
+ * Create new document
+ *
+ * @param string $query
+ * @param string $options
+ * @return boolean
+ */
public function create($query, array $options = array()) {
+ $defaults = array('safe' => false, 'fsync' => false);
+ $options += $defaults;
+ $this->_checkConnection();
+
$params = compact('query', 'options');
- $conn =& $this->_connection;
- $db =& $this->_db;
+ $_config = $this->_config;
+ $_exp = $this->_classes['exporter'];
- return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, &$db) {
- $query = $params['query'];
+ return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config, $_exp) {
+ $query = $params['query'];
$options = $params['options'];
- $data = $query->data();
- $params = $query->export($self);
- $result = $db->{$params['table']}->insert($data, true);
+ $args = $query->export($self, array('keys' => array('source', 'data')));
+ $data = $_exp::get('create', $args['data']);
+ $source = $args['source'];
- if (isset($result['ok']) && $result['ok'] === 1.0) {
- $id = $data['_id'];
- $query->record()->update(is_object($id) ? $id->__toString() : null);
+ if ($source == "{$_config['gridPrefix']}.files" && isset($data['create']['file'])) {
+ $result = array('ok' => true);
+ $data['create']['_id'] = $self->invokeMethod('_saveFile', array($data['create']));
+ } else {
+ $result = $self->connection->{$source}->insert($data['create'], $options);
+ }
+
+ if ($result === true || isset($result['ok']) && (boolean) $result['ok'] === true) {
+ if ($query->entity()) {
+ $query->entity()->update($data['create']['_id']);
+ }
return true;
}
return false;
});
}
+ protected function _saveFile($data) {
+ $uploadKeys = array('name', 'type', 'tmp_name', 'error', 'size');
+ $grid = $this->connection->getGridFS();
+ $file = null;
+ $method = null;
+
+ switch (true) {
+ case (is_array($data['file']) && array_keys($data['file']) == $uploadKeys):
+ if (!$data['file']['error'] && is_uploaded_file($data['file']['tmp_name'])) {
+ $method = 'storeFile';
+ $file = $data['file']['tmp_name'];
+ $data['filename'] = $data['file']['name'];
+ }
+ break;
+ case (is_string($data['file']) && file_exists($data['file'])):
+ $method = 'storeFile';
+ $file = $data['file'];
+ break;
+ case $data['file']:
+ $method = 'storeBytes';
+ $file = $data['file'];
+ break;
+ }
+
+ if (!$method || !$file) {
+ return;
+ }
+
+ if (isset($data['_id'])) {
+ $data += (array) get_object_vars($grid->get($data['_id']));
+ $grid->delete($data['_id']);
+ }
+ unset($data['file']);
+ return $grid->{$method}($file, $data);
+ }
+
+ /**
+ * Read from document
+ *
+ * @param string $query
+ * @param string $options
+ * @return object
+ */
public function read($query, array $options = array()) {
+ $this->_checkConnection();
$defaults = array('return' => 'resource');
$options += $defaults;
- $db =& $this->_db;
- $conn =& $this->_connection;
$params = compact('query', 'options');
+ $_config = $this->_config;
- return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, &$db) {
+ return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config) {
$query = $params['query'];
$options = $params['options'];
- $params = $query->export($self);
-
- $table = $params['table'];
- $conditions = $params['conditions'];
-
- if ($group = $params['group']) {
- $group += array(
- '$reduce' => $params['reduce'] ?: null, 'initial' => $params['initial'] ?: null
- );
-
- $stats = $db->command(array('group' => $group + array(
- 'ns' => $table,
- 'cond' => $conditions
- )));
- $data = isset($stats['retval']) ? $stats['retval'] : null;
- unset($stats['retval']);
-
- $params = array('document', $query, compact('data', 'stats') + array(
- 'model' => $options['model']
- ));
- return $self->invokeMethod('_result', $params);
+ $args = $query->export($self);
+ $source = $args['source'];
+
+ if ($group = $args['group']) {
+ $result = $self->invokeMethod('_group', array($group, $args, $options));
+ $config = array('class' => 'set') + compact('query') + $result;
+ return $self->item($query->model(), $config['data'], $config);
}
- $result = $db->{$table}->find($conditions, $params['fields']);
+ $collection = $self->connection->{$source};
+
+ if ($source == "{$_config['gridPrefix']}.files") {
+ $collection = $self->connection->getGridFS();
+ }
+ $result = $collection->find($args['conditions'], $args['fields']);
if ($query->calculate()) {
return $result;
}
+ $resource = $result->sort($args['order'])->limit($args['limit'])->skip($args['offset']);
+ $result = $self->invokeMethod('_instance', array('result', compact('resource')));
+ $config = compact('result', 'query') + array('class' => 'set');
+ return $self->item($query->model(), array(), $config);
+ });
+ }
- $order = $params['order'];
- $limit = $params['limit'];
- $offset = $params['offset'];
- $result = $result->sort($order)->limit($limit)->skip($offset);
- $options = compact('result') + array('model' => $options['model']);
+ protected function _group($group, $args, $options) {
+ $conditions = $args['conditions'];
+ $group += array('$reduce' => $args['reduce'], 'initial' => $args['initial']);
+ $command = array('group' => $group + array('ns' => $args['source'], 'cond' => $conditions));
- return $self->invokeMethod('_result', array('document', $query, $options));
- });
+ $stats = $this->connection->command($command);
+ $data = isset($stats['retval']) ? $stats['retval'] : null;
+ unset($stats['retval']);
+ return compact('data', 'stats');
}
+ /**
+ * Update document
+ *
+ * @param string $query
+ * @param array $options
+ * @return boolean
+ */
public function update($query, array $options = array()) {
- $db =& $this->_db;
- $conn =& $this->_connection;
+ $defaults = array('upsert' => false, 'multiple' => true, 'safe' => false, 'fsync' => false);
+ $options += $defaults;
+ $this->_checkConnection();
+
$params = compact('query', 'options');
+ $_config = $this->_config;
+ $_exp = $this->_classes['exporter'];
- return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, &$db) {
- $query = $params['query'];
+ return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config, $_exp) {
$options = $params['options'];
+ $query = $params['query'];
+ $args = $query->export($self, array('keys' => array('conditions', 'source', 'data')));
+ $source = $args['source'];
+ $data = $args['data'];
- $params = $query->export($self);
- $data = $query->data();
+ if ($query->entity()) {
+ $data = $_exp::get('update', $data);
+ }
- if ($db->{$params['table']}->update($params['conditions'], $data)) {
- $query->record()->update();
+ if ($source == "{$_config['gridPrefix']}.files" && isset($data['update']['file'])) {
+ $args['data']['_id'] = $self->invokeMethod('_saveFile', array($data['update']));
+ }
+ $update = $query->entity() ? $_exp::toCommand($data) : $data;
+
+ if ($options['multiple'] && !preg_grep('/^\$/', array_keys($update))) {
+ $update = array('$set' => $update);
+ }
+ if ($self->connection->{$source}->update($args['conditions'], $update, $options)) {
+ $query->entity() ? $query->entity()->update() : null;
return true;
}
return false;
});
}
+ /**
+ * Delete document
+ *
+ * @param string $query
+ * @param string $options
+ * @return boolean
+ */
public function delete($query, array $options = array()) {
- $db =& $this->_db;
- $conn =& $this->_connection;
- $params = compact('query', 'options');
+ $this->_checkConnection();
+ $defaults = array('justOne' => false, 'safe' => false, 'fsync' => false);
+ $options = array_intersect_key($options + $defaults, $defaults);
- return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, &$db) {
+ return $this->_filter(__METHOD__, compact('query', 'options'), function($self, $params) {
$query = $params['query'];
$options = $params['options'];
-
- $params = $query->export($self);
- $params['conditions'] = $self->invokeMethod('_toMongoId', array($params['conditions']));
- return $db->{$params['table']}->remove($params['conditions']);
+ $args = $query->export($self, array('keys' => array('source', 'conditions')));
+ return $self->connection->{$args['source']}->remove($args['conditions'], $options);
});
}
/**
- * Returns a newly-created `Document` object, bound to a model and populated with default data
- * and options.
- *
- * @param string $model A fully-namespaced class name representing the model class to which the
- * `Document` object will be bound.
- * @param array $data The default data with which the new `Document` should be populated.
- * @param array $options Any additional options to pass to the `Document`'s constructor.
- * @return object Returns a new, un-saved `Document` object bound to the model class specified
- * in `$model`.
+ * Executes calculation-related queries, such as those required for `count`.
+ *
+ * @param string $type Only accepts `count`.
+ * @param mixed $query The query to be executed.
+ * @param array $options Optional arguments for the `read()` query that will be executed
+ * to obtain the calculation result.
+ * @return integer Result of the calculation.
*/
- public function item($model, array $data = array(), array $options = array()) {
- $class = $this->_classes['document'];
- return new $class(compact('model', 'data') + $options);
- }
-
public function calculation($type, $query, array $options = array()) {
$query->calculate($type);
@@ -293,37 +568,37 @@ class MongoDb extends \lithium\data\Source {
}
/**
- * Creates a link between two `Document` objects.
+ * Document relationships.
*
- * @param object $object
- * @param object $related
- * @param array $options
- * @return boolean Returns `true` if MongoDB was able to create a link between the two
- * documents, otherwise `false`, if the link failed or if both `$object` and `$related`
- * are not top-level, pre-existing `Document` objects.
+ * @param string $class
+ * @param string $type Relationship type, e.g. `belongsTo`.
+ * @param string $name
+ * @param array $config
+ * @return array
*/
- public function link($object, $related, array $options = array()) {
- if (!$object->_id || !$related->_id) {
- return false;
- }
- }
+ public function relationship($class, $type, $name, array $config = array()) {
+ $keys = Inflector::camelize($type == 'belongsTo' ? $class::meta('name') : $name, false);
- public function result($type, $resource, $context) {
- if (!is_object($resource)) {
- return null;
- }
+ $config += compact('name', 'type', 'keys');
+ $config['from'] = $class;
+ $relationship = $this->_classes['relationship'];
- switch ($type) {
- case 'next':
- return $resource->hasNext() ? $resource->getNext() : null;
- case 'close':
- unset($resource);
- return null;
- default:
- return parent::result($type, $resource, $context);
- }
+ $defaultLinks = array(
+ 'hasOne' => $relationship::LINK_EMBEDDED,
+ 'hasMany' => $relationship::LINK_EMBEDDED,
+ 'belongsTo' => $relationship::LINK_CONTAINED
+ );
+ $config += array('link' => $defaultLinks[$type]);
+ return new $relationship($config);
}
+ /**
+ * Formats `group` clauses for MongoDB.
+ *
+ * @param string|array $group The group clause.
+ * @param object $context
+ * @return array Formatted `group` clause.
+ */
public function group($group, $context) {
if (!$group) {
return;
@@ -342,52 +617,189 @@ class MongoDb extends \lithium\data\Source {
return array('key' => $group);
}
+ /**
+ * Maps incoming conditions with their corresponding MongoDB-native operators.
+ *
+ * @param array $conditions Array of conditions
+ * @param object $context Context with which this method was called; currently
+ * inspects the return value of `$context->type()`.
+ * @return array Transformed conditions
+ */
public function conditions($conditions, $context) {
+ $schema = array();
+ $model = null;
+
if (!$conditions) {
return array();
}
-
- if ($context->type() == 'create') {
- return $this->_toMongoId($conditions);
+ if ($code = $this->_isMongoCode($conditions)) {
+ return $code;
+ }
+ if ($context) {
+ $model = $context->model();
+ $schema = $context->schema();
}
- $conditions = $this->_toMongoId($conditions);
+ return $this->_conditions($conditions, $model, $schema, $context);
+ }
+
+ protected function _conditions($conditions, $model, $schema, $context) {
+ $castOpts = compact('schema') + array('first' => true, 'arrays' => false);
foreach ($conditions as $key => $value) {
- if ($key[0] === '$') {
+ if ($key === '$or' || $key === 'or' || $key === '||') {
+ foreach ($value as $i => $or) {
+ $value[$i] = $this->_conditions($or, $model, $schema, $context);
+ }
+ unset($conditions[$key]);
+ $conditions['$or'] = $value;
+ continue;
+ }
+ if (is_object($value)) {
+ continue;
+ }
+ if (!is_array($value)) {
+ $conditions[$key] = $this->cast(null, array($key => $value), $castOpts);
continue;
}
- if (is_array($value) && strpos(key($value), '$') !== 0) {
- $conditions[$key] = array('$in' => $value);
+ $current = key($value);
+ $isOpArray = (isset($this->_operators[$current]) || $current[0] === '$');
+
+ if (!$isOpArray) {
+ $data = array($key => $value);
+ $conditions[$key] = array('$in' => $this->cast($model, $data, $castOpts));
+ continue;
+ }
+ $operations = array();
+
+ foreach ($value as $op => $val) {
+ if (is_object($result = $this->_operator($model, $key, $op, $val, $schema))) {
+ $operations = $result;
+ break;
+ }
+ $operations += $this->_operator($model, $key, $op, $val, $schema);
}
+ $conditions[$key] = $operations;
}
return $conditions;
}
+ protected function _isMongoCode($conditions) {
+ if ($conditions instanceof MongoCode) {
+ return array('$where' => $conditions);
+ }
+ if (is_string($conditions)) {
+ return array('$where' => new MongoCode($conditions));
+ }
+ }
+
+ protected function _operator($model, $key, $op, $value, $schema) {
+ $castOpts = compact('schema') + array('first' => true, 'arrays' => false);
+
+ switch (true) {
+ case !isset($this->_operators[$op]):
+ return array($op => $this->cast($model, array($key => $value), $castOpts));
+ case is_callable($this->_operators[$op]):
+ return $this->_operators[$op]($key, $value);
+ case is_array($this->_operators[$op]):
+ $format = (is_array($value)) ? 'multiple' : 'single';
+ $operator = $this->_operators[$op][$format];
+ break;
+ default:
+ $operator = $this->_operators[$op];
+ break;
+ }
+ return array($operator => $value);
+ }
+
+ /**
+ * Return formatted identifiers for fields.
+ *
+ * MongoDB does nt require field identifer escaping; as a result,
+ * this method is not implemented.
+ *
+ * @param array $fields Fields to be parsed
+ * @param object $context
+ * @return array Parsed fields array
+ */
public function fields($fields, $context) {
return $fields ?: array();
}
+ /**
+ * Return formatted clause for limit.
+ *
+ * MongoDB does nt require limit identifer formatting; as a result,
+ * this method is not implemented.
+ *
+ * @param mixed $limit The `limit` clause to be formatted
+ * @param object $context
+ * @return mixed Formatted `limit` clause.
+ */
public function limit($limit, $context) {
return $limit ?: 0;
}
+ /**
+ * Return formatted clause for order.
+ *
+ * @param mixed $order The `order` clause to be formatted
+ * @param object $context
+ * @return mixed Formatted `order` clause.
+ */
public function order($order, $context) {
+ switch (true) {
+ case !$order:
+ return array();
+ case is_string($order):
+ return array($order => 1);
+ case is_array($order):
+ foreach ($order as $key => $value) {
+ if (!is_string($key)) {
+ unset($order[$key]);
+ $order[$value] = 1;
+ continue;
+ }
+ if (is_string($value)) {
+ $order[$key] = strtoupper($value) == 'ASC' ? 1 : -1;
+ }
+ }
+ break;
+ }
return $order ?: array();
}
- protected function _result($type, $query, $config = array()) {
- $defaults = array('handle' => &$this, 'exists' => true);
- $class = $this->_classes[$type];
- return new $class(compact('query') + $config + $defaults);
+ public function cast($entity, array $data, array $options = array()) {
+ $defaults = array('schema' => null, 'first' => false, 'pathKey' => null);
+ $options += $defaults;
+ $model = null;
+
+ if (!$data) {
+ return $data;
+ }
+
+ if (is_string($entity)) {
+ $model = $entity;
+ $entity = null;
+ $options['schema'] = $options['schema'] ?: $model::schema();
+ }
+ if ($entity && !$options['schema']) {
+ $options['schema'] = $entity->schema();
+ }
+ if ($entity) {
+ $model = $entity->model();
+ }
+ $schema = $options['schema'] ?: array('_id' => array('type' => 'id'));
+ unset($options['schema']);
+ $exporter = $this->_classes['exporter'];
+ $options += compact('model') + array('handlers' => $this->_handlers);
+
+ return parent::cast($entity, $exporter::cast($data, $schema, $this, $options), $options);
}
- protected function _toMongoId($data) {
- if (isset($data['_id']) && !is_object($data['_id'])) {
- if (preg_match('/^[0-9a-f]{24}$/', (string) $data['_id'])) {
- $data['_id'] = new MongoId($data['_id']);
- }
+ protected function _checkConnection() {
+ if (!$this->_isConnected && !$this->connect()) {
+ throw new NetworkException("Could not connect to the database.");
}
- return $data;
}
}
diff --git a/libraries/lithium/data/source/database/Result.php b/libraries/lithium/data/source/database/Result.php
new file mode 100644
index 0000000..2639fa9
--- /dev/null
+++ b/libraries/lithium/data/source/database/Result.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\database;
+
+abstract class Result extends \lithium\core\Object implements \Iterator {
+
+ protected $_iterator = 0;
+
+ protected $_current = null;
+
+ protected $_resource = null;
+
+ protected $_autoConfig = array('resource');
+
+ public function __construct(array $config = array()) {
+ $defaults = array('resource' => null);
+ parent::__construct($config + $defaults);
+ }
+
+ public function resource() {
+ return $this->_resource;
+ }
+
+ public function rewind() {
+ return null;
+ }
+
+ public function valid() {
+ return !empty($this->_resource);
+ }
+
+ public function current() {
+ return $this->_current;
+ }
+
+ public function key() {
+ return $this->_iterator;
+ }
+
+ public function next() {
+ if ($this->_current = $this->_next()) {
+ $this->_iterator++;
+ return $this->_current;
+ }
+ $this->_close();
+ $this->_resource = null;
+ }
+
+ public function __destruct() {
+ $this->_close();
+ $this->_resource = null;
+ }
+
+ abstract protected function _next();
+
+ abstract protected function _close();
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/database/adapter/MySql.php b/libraries/lithium/data/source/database/adapter/MySql.php
index 039d0a8..8172010 100644
--- a/libraries/lithium/data/source/database/adapter/MySql.php
+++ b/libraries/lithium/data/source/database/adapter/MySql.php
@@ -2,16 +2,31 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data\source\database\adapter;
-use \Exception;
+use lithium\data\model\QueryException;
+/**
+ * Extends the `Database` class to implement the necessary SQL-formatting and resultset-fetching
+ * features for working with MySQL databases.
+ *
+ * For more information on configuring the database connection, see the `__construct()` method.
+ *
+ * @see lithium\data\source\database\adapter\MySql::__construct()
+ */
class MySql extends \lithium\data\source\Database {
+ protected $_classes = array(
+ 'entity' => 'lithium\data\entity\Record',
+ 'set' => 'lithium\data\collection\RecordSet',
+ 'relationship' => 'lithium\data\model\Relationship',
+ 'result' => 'lithium\data\source\database\adapter\my_sql\Result'
+ );
+
/**
* MySQL column type definitions.
*
@@ -34,6 +49,13 @@ class MySql extends \lithium\data\source\Database {
);
/**
+ * Pair of opening and closing quote characters used for quoting identifiers in queries.
+ *
+ * @var array
+ */
+ protected $_quotes = array('`', '`');
+
+ /**
* MySQL-specific value denoting whether or not table aliases should be used in DELETE and
* UPDATE queries.
*
@@ -51,21 +73,42 @@ class MySql extends \lithium\data\source\Database {
* see `lithium\data\source\Database` and `lithium\data\Source`. Available options
* defined by this class:
* - `'database'`: The name of the database to connect to. Defaults to 'lithium'.
- * - `'host'`: The IP or machine name where MySQL is running. Defaults to 'localhost'.
+ * - `'host'`: The IP or machine name where MySQL is running, followed by a colon,
+ * followed by a port number or socket. Defaults to `'localhost:3306'`.
* - `'persistent'`: If a persistent connection (if available) should be made.
* Defaults to true.
- * - `'port'`: The port number MySQL is listening on. The default is '3306'.
*
* Typically, these parameters are set in `Connections::add()`, when adding the adapter to the
* list of active connections.
* @return The adapter instance.
*/
public function __construct(array $config = array()) {
- $defaults = array('port' => '3306', 'encoding' => null);
+ $defaults = array('host' => 'localhost:3306', 'encoding' => null);
parent::__construct($config + $defaults);
}
/**
+ * Check for required PHP extension, or supported database feature.
+ *
+ * @param string $feature Test for support for a specific feature, i.e. `"transactions"` or
+ * `"arrays"`.
+ * @return boolean Returns `true` if the particular feature (or if MySQL) support is enabled,
+ * otherwise `false`.
+ */
+ public static function enabled($feature = null) {
+ if (!$feature) {
+ return extension_loaded('mysql');
+ }
+ $features = array(
+ 'arrays' => false,
+ 'transactions' => false,
+ 'booleans' => true,
+ 'relationships' => true,
+ );
+ return isset($features[$feature]) ? $features[$feature] : null;
+ }
+
+ /**
* Connects to the database using the options provided to the class constructor.
*
* @return boolean Returns `true` if a database connection could be established, otherwise
@@ -74,29 +117,34 @@ class MySql extends \lithium\data\source\Database {
public function connect() {
$config = $this->_config;
$this->_isConnected = false;
- $host = $config['host'] . ':' . $config['port'];
+ $host = $config['host'];
if (!$config['database']) {
return false;
}
- if ($config['persistent']) {
- $this->_connection = mysql_connect($host, $config['login'], $config['password'], true);
+ if (!$config['persistent']) {
+ $this->connection = mysql_connect($host, $config['login'], $config['password'], true);
} else {
- $this->_connection = mysql_pconnect($host, $config['login'], $config['password']);
+ $this->connection = mysql_pconnect($host, $config['login'], $config['password']);
+ }
+
+ if (!$this->connection) {
+ return false;
}
- if (mysql_select_db($config['database'], $this->_connection)) {
+ if (mysql_select_db($config['database'], $this->connection)) {
$this->_isConnected = true;
+ } else {
+ return false;
}
if ($config['encoding']) {
$this->encoding($config['encoding']);
}
- $this->_useAlias = (boolean) version_compare(
- mysql_get_server_info($this->_connection), "4.1", ">="
- );
+ $info = mysql_get_server_info($this->connection);
+ $this->_useAlias = (boolean) version_compare($info, "4.1", ">=");
return $this->_isConnected;
}
@@ -107,7 +155,7 @@ class MySql extends \lithium\data\source\Database {
*/
public function disconnect() {
if ($this->_isConnected) {
- $this->_isConnected = !mysql_close($this->_connection);
+ $this->_isConnected = !mysql_close($this->connection);
return !$this->_isConnected;
}
return true;
@@ -121,12 +169,22 @@ class MySql extends \lithium\data\source\Database {
* @filter This method can be filtered.
*/
public function entities($model = null) {
- $config = $this->_config;
- $method = function($self, $params, $chain) use ($config) {
- $name = $this->name($config['database']);
- return $self->query("SHOW TABLES FROM {$name};");
- };
- return $this->_filter(__METHOD__, compact('model'), $method);
+ $_config = $this->_config;
+ $params = compact('model');
+
+ return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config) {
+ $name = $self->name($_config['database']);
+
+ if (!$result = $self->invokeMethod('_execute', array("SHOW TABLES FROM {$name};"))) {
+ return null;
+ }
+ $entities = array();
+
+ while ($data = $result->next()) {
+ list($entities[]) = $data;
+ }
+ return $entities;
+ });
}
/**
@@ -142,28 +200,24 @@ class MySql extends \lithium\data\source\Database {
* - `'type'`: The field type name
* @filter This method can be filtered.
*/
- public function describe($entity, $meta = array()) {
+ public function describe($entity, array $meta = array()) {
$params = compact('entity', 'meta');
- return $this->_filter(__METHOD__, $params, function($self, $params, $chain) {
+ return $this->_filter(__METHOD__, $params, function($self, $params) {
extract($params);
$name = $self->invokeMethod('_entityName', array($entity));
- $columns = $self->read("DESCRIBE {$name}", array('return' => 'array'));
+ $columns = $self->read("DESCRIBE {$name}", array('return' => 'array', 'schema' => array(
+ 'field', 'type', 'null', 'key', 'default', 'extra'
+ )));
$fields = array();
foreach ($columns as $column) {
- preg_match('/(?P<type>\w+)(\((?P<length>\d+)\))?/', $column['Type'], $match);
- $filtered = array_intersect_key($match, array('type' => null, 'length' => null));
- $match = $filtered + array('length' => null);
- $match = $self->invokeMethod('_column', array($match['type'])) + $match;
-
- $fields[$column['Field']] = $match + array(
- 'null' => ($column['Null'] == 'YES' ? true : false),
- 'default' => $column['Default'],
+ $match = $self->invokeMethod('_column', array($column['type']));
+
+ $fields[$column['field']] = $match + array(
+ 'null' => ($column['null'] == 'YES' ? true : false),
+ 'default' => $column['default'],
);
- //if (!empty($column['Key']) && isset($this->index[$column[0]['Key']])) {
- // $fields[$column['Field']]['key'] = $this->index[$column[0]['Key']];
- //}
}
return $fields;
});
@@ -180,43 +234,26 @@ class MySql extends \lithium\data\source\Database {
$encodingMap = array('UTF-8' => 'utf8');
if (empty($encoding)) {
- $encoding = mysql_client_encoding($this->_connection);
+ $encoding = mysql_client_encoding($this->connection);
return ($key = array_search($encoding, $encodingMap)) ? $key : $encoding;
}
$encoding = isset($encodingMap[$encoding]) ? $encodingMap[$encoding] : $encoding;
- return mysql_set_charset($encoding, $this->_connection);
- }
-
- public function result($type, $resource, $context) {
- if (!is_resource($resource)) {
- return null;
- }
-
- switch ($type) {
- case 'next':
- $result = mysql_fetch_row($resource);
- break;
- case 'close':
- mysql_free_result($resource);
- $result = null;
- break;
- default:
- $result = parent::result($type, $resource, $context);
- break;
- }
- return $result;
+ return mysql_set_charset($encoding, $this->connection);
}
+ /**
+ * Converts a given value into the proper type based on a given schema definition.
+ *
+ * @see lithium\data\source\Database::schema()
+ * @param mixed $value The value to be converted. Arrays will be recursively converted.
+ * @param array $schema Formatted array from `lithium\data\source\Database::schema()`
+ * @return mixed Value with converted type.
+ */
public function value($value, array $schema = array()) {
- if (is_array($value)) {
- return parent::value($value, $schema);
- }
- $result = parent::value($value, $schema);;
-
- if (is_string($result)) {
- return "'" . mysql_real_escape_string($value, $this->_connection) . "'";
+ if (($result = parent::value($value, $schema)) !== null) {
+ return $result;
}
- return $result;
+ return "'" . mysql_real_escape_string((string) $value, $this->connection) . "'";
}
/**
@@ -234,10 +271,10 @@ class MySql extends \lithium\data\source\Database {
}
$result = array();
- $count = mysql_num_fields($resource);
+ $count = mysql_num_fields($resource->resource());
for ($i = 0; $i < $count; $i++) {
- $result[] = mysql_field_name($resource, $i);
+ $result[] = mysql_field_name($resource->resource(), $i);
}
return $result;
}
@@ -248,41 +285,61 @@ class MySql extends \lithium\data\source\Database {
* @return array
*/
public function error() {
- if (mysql_error($this->_connection)) {
- return array(mysql_errno($this->_connection), mysql_error($this->_connection));
+ if (mysql_error($this->connection)) {
+ return array(mysql_errno($this->connection), mysql_error($this->connection));
}
return null;
}
+ public function alias($alias, $context) {
+ if ($context->type() == 'update' || $context->type() == 'delete') {
+ return;
+ }
+ return parent::alias($alias, $context);
+ }
+
/**
- * Quotes identifiers.
- *
- * Currently, this method simply returns the identifier.
- *
- * @param string $name The identifier to quote.
- * @return string The quoted identifier.
+ * @todo Eventually, this will need to rewrite aliases for DELETE and UPDATE queries, same with
+ * order().
+ * @param string $conditions
+ * @param string $context
+ * @param array $options
+ * @return void
*/
- public function name($name) {
- return $name;
+ public function conditions($conditions, $context, array $options = array()) {
+ return parent::conditions($conditions, $context, $options);
}
+ /**
+ * Execute a given query.
+ *
+ * @see lithium\data\source\Database::renderCommand()
+ * @param string $sql The sql string to execute
+ * @param array $options Available options:
+ * - 'buffered': If set to `false` uses mysql_unbuffered_query which
+ * sends the SQL query query to MySQL without automatically fetching and buffering the
+ * result rows as `mysql_query()` does (for less memory usage).
+ * @return resource Returns the result resource handle if the query is successful.
+ */
protected function _execute($sql, array $options = array()) {
$defaults = array('buffered' => true);
$options += $defaults;
- $conn =& $this->_connection;
- $params = compact('sql', 'options');
+ return $this->_filter(__METHOD__, compact('sql', 'options'), function($self, $params) {
+ $sql = $params['sql'];
+ $options = $params['options'];
- return $this->_filter(__METHOD__, $params, function($self, $params, $chain) use (&$conn) {
- extract($params);
$func = ($options['buffered']) ? 'mysql_query' : 'mysql_unbuffered_query';
- $result = $func($sql, $conn);
+ $resource = $func($sql, $self->connection);
- if (!(is_resource($result) || $result === true)) {
- list($code, $error) = $self->error();
- throw new Exception("{$sql}: {$error}", $code);
+ if ($resource === true) {
+ return true;
}
- return $result;
+ if (is_resource($resource)) {
+ return $self->invokeMethod('_instance', array('result', compact('resource')));
+ }
+ list($code, $error) = $self->error();
+ throw new QueryException("{$sql}: {$error}", $code);
});
}
@@ -308,30 +365,32 @@ class MySql extends \lithium\data\source\Database {
*/
protected function _insertId($query) {
$resource = $this->_execute('SELECT LAST_INSERT_ID() AS insertID');
- list($id) = $this->result('next', $resource, null);
- $this->result('close', $resource, null);
-
- if (!empty($id) && $id !== '0') {
- return $id;
- }
+ list($id) = $resource->next();
+ return ($id && $id !== '0') ? $id : null;
}
/**
* Converts database-layer column types to basic types.
*
* @param string $real Real database-layer column type (i.e. `"varchar(255)"`)
- * @return string Abstract column type (i.e. "string")
+ * @return array Column type (i.e. "string") plus 'length' when appropriate.
*/
protected function _column($real) {
if (is_array($real)) {
return $real['type'] . (isset($real['length']) ? "({$real['length']})" : '');
}
- if (!preg_match('/(?P<type>[^(]+)(?:\((?P<length>[^)]+)\))?/', $real, $column)) {
+ if (!preg_match('/(?P<type>\w+)(?:\((?P<length>[\d,]+)\))?/', $real, $column)) {
return $real;
}
$column = array_intersect_key($column, array('type' => null, 'length' => null));
+ if (isset($column['length']) && $column['length']) {
+ $length = explode(',', $column['length']) + array(null, null);
+ $column['length'] = $length[0] ? intval($length[0]) : null;
+ $length[1] ? $column['precision'] = intval($length[1]) : null;
+ }
+
switch (true) {
case in_array($column['type'], array('date', 'time', 'datetime', 'timestamp')):
return $column;
@@ -361,6 +420,12 @@ class MySql extends \lithium\data\source\Database {
return $column;
}
+ /**
+ * Helper method that retrieves an entity's name via its metadata.
+ *
+ * @param string $entity Entity name.
+ * @return string Name.
+ */
protected function _entityName($entity) {
if (class_exists($entity, false) && method_exists($entity, 'meta')) {
$entity = $entity::meta('name');
diff --git a/libraries/lithium/data/source/database/adapter/Sqlite3.php b/libraries/lithium/data/source/database/adapter/Sqlite3.php
index eaaa22e..37f9517 100644
--- a/libraries/lithium/data/source/database/adapter/Sqlite3.php
+++ b/libraries/lithium/data/source/database/adapter/Sqlite3.php
@@ -2,14 +2,18 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*
*/
namespace lithium\data\source\database\adapter;
-use \Exception;
+use SQLite3 as SQLite;
+use SQLite3Result;
+use lithium\data\model\QueryException;
+use lithium\data\source\database\adapter\sqlite3\Result;
+
/**
* Sqlite database driver
*
@@ -29,7 +33,9 @@ class Sqlite3 extends \lithium\data\source\Database {
'integer' => array('name' => 'integer', 'limit' => 11, 'formatter' => 'intval'),
'float' => array('name' => 'float', 'formatter' => 'floatval'),
'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
- 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
+ 'timestamp' => array(
+ 'name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'
+ ),
'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'),
'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'),
'binary' => array('name' => 'blob'),
@@ -39,6 +45,9 @@ class Sqlite3 extends \lithium\data\source\Database {
/**
* Constructs the Sqlite adapter
*
+ * @see lithium\data\source\Database::__construct()
+ * @see lithium\data\Source::__construct()
+ * @see lithium\data\Connections::add()
* @param array $config Configuration options for this class. For additional configuration,
* see `lithium\data\source\Database` and `lithium\data\Source`. Available options
* defined by this class:
@@ -51,18 +60,14 @@ class Sqlite3 extends \lithium\data\source\Database {
* Typically, these parameters are set in `Connections::add()`, when adding the adapter to the
* list of active connections.
* @return The adapter instance.
- *
- * @see lithium\data\source\Database::__construct()
- * @see lithium\data\Source::__construct()
- * @see lithium\data\Connections::add()
*/
public function __construct(array $config = array()) {
$defaults = array(
'database' => '',
- 'flags' => NULL,
+ 'flags' => SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE,
'key' => NULL
);
- parent::__construct((array) $config + $defaults);
+ parent::__construct($config + $defaults);
}
/**
@@ -74,31 +79,56 @@ class Sqlite3 extends \lithium\data\source\Database {
$config = $this->_config;
$this->_isConnected = false;
- if ($this->_connection = new \SQLite3($config['database'], $config['flags'], $config['key'])) {
+ if ($this->connection = new SQLite($config['database'], $config['flags'], $config['key'])) {
$this->_isConnected = true;
}
return $this->_isConnected;
}
+ /**
+ * Disconnects the adapter from the database.
+ *
+ * @return boolean True on success, else false.
+ */
public function disconnect() {
if ($this->_isConnected) {
- $this->_isConnected = !$this->_connection->close();
+ $this->_isConnected = !$this->connection->close();
return !$this->_isConnected;
}
return true;
}
+ /**
+ * Returns the list of tables in the currently-connected database.
+ *
+ * @param string $model The fully-name-spaced class name of the model object making the request.
+ * @return array Returns an array of objects to which models can connect.
+ * @filter This method can be filtered.
+ */
public function entities($model = null) {
$config = $this->_config;
- $method = function($self, $params, $chain) use ($config) {
+ $method = function($self, $params) use ($config) {
return $self->query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
};
return $this->_filter(__METHOD__, compact('model'), $method);
}
- public function describe($entity, $meta = array()) {
+ /**
+ * Gets the column schema for a given Sqlite3 table.
+ *
+ * @param mixed $entity Specifies the table name for which the schema should be returned, or
+ * the class name of the model object requesting the schema, in which case the model
+ * class will be queried for the correct table name.
+ * @param array $meta
+ * @return array Returns an associative array describing the given table's schema, where the
+ * array keys are the available fields, and the values are arrays describing each
+ * field, containing the following keys:
+ * - `'type'`: The field type name
+ * @filter This method can be filtered.
+ */
+ public function describe($entity, array $meta = array()) {
$params = compact('entity', 'meta');
- return $this->_filter(__METHOD__, $params, function($self, $params, $chain) {
+ return $this->_filter(__METHOD__, $params, function($self, $params) {
extract($params);
$name = $self->invokeMethod('_entityName', array($entity));
@@ -106,7 +136,7 @@ class Sqlite3 extends \lithium\data\source\Database {
$fields = array();
foreach ($columns as $column) {
- list($type, $length) = explode('(', $column['type']);
+ list($type, $length) = explode('(', $column['type']) + array('', '');
$length = trim($length, ')');
$fields[$column['name']] = array(
'type' => $type,
@@ -119,46 +149,49 @@ class Sqlite3 extends \lithium\data\source\Database {
});
}
+ /**
+ * Get the last insert id from the database.
+ *
+ * @param object $query The given query, usually an instance of `lithium\data\model\Query`.
+ * @return void
+ */
protected function _insertId($query) {
+ return $this->connection->lastInsertRowID();
}
+ /**
+ * Gets or sets the encoding for the connection.
+ *
+ * @param string $encoding If setting the encoding, this is the name of the encoding to set,
+ * i.e. `'utf8'` or `'UTF-8'` (both formats are valid).
+ * @return boolean|string If setting the encoding; returns `true` on success, or `false` on
+ * failure. When getting, returns the encoding as a string.
+ */
public function encoding($encoding = null) {
$encodingMap = array('UTF-8' => 'utf8');
- if (empty($encoding)) {
- $encoding = $this->_connection->querySingle('PRAGMA encoding');
+ if (!$encoding) {
+ $encoding = $this->connection->querySingle('PRAGMA encoding');
return ($key = array_search($encoding, $encodingMap)) ? $key : $encoding;
}
$encoding = isset($encodingMap[$encoding]) ? $encodingMap[$encoding] : $encoding;
- $this->_connection->exec("PRAGMA encoding = \"{$encoding}\"");
- return $this->_connection->querySingle("PRAGMA encoding");
- }
-
- public function result($type, $resource, $context) {
- if (!($resource instanceof \SQLite3Result)) {
- return null;
- }
-
- switch ($type) {
- case 'next':
- $result = $resource->fetchArray(SQLITE3_ASSOC);
- break;
- case 'close':
- $resource->finalize();
- $result = null;
- break;
- default:
- $result = parent::result($type, $resource, $context);
- break;
- }
- return $result;
+ $this->connection->exec("PRAGMA encoding = \"{$encoding}\"");
+ return $this->connection->querySingle("PRAGMA encoding");
}
+ /**
+ * Converts a given value into the proper type based on a given schema definition.
+ *
+ * @see lithium\data\source\Database::schema()
+ * @param mixed $value The value to be converted. Arrays will be recursively converted.
+ * @param array $schema Formatted array from `lithium\data\source\Database::schema()`
+ * @return mixed Value with converted type.
+ */
public function value($value, array $schema = array()) {
if (is_array($value)) {
return parent::value($value, $schema);
}
- return $this->_connection->escapeString($value);
+ return "'" . $this->connection->escapeString($value) . "'";
}
/**
@@ -190,8 +223,8 @@ class Sqlite3 extends \lithium\data\source\Database {
* @return array
*/
public function error() {
- if ($this->_connection->lastErrorMsg()) {
- return array($this->_connection->lastErrorCode(), $this->_connection->lastErrorMsg());
+ if ($this->connection->lastErrorMsg()) {
+ return array($this->connection->lastErrorCode(), $this->connection->lastErrorMsg());
}
return null;
}
@@ -208,18 +241,26 @@ class Sqlite3 extends \lithium\data\source\Database {
return $name;
}
+ /**
+ * Execute a given query.
+ *
+ * @see lithium\data\source\Database::renderCommand()
+ * @param string $sql The sql string to execute
+ * @param array $options No available options.
+ * @return resource
+ */
protected function _execute($sql, array $options = array()) {
$params = compact('sql', 'options');
- $conn =& $this->_connection;
+ $conn =& $this->connection;
- return $this->_filter(__METHOD__, $params, function($self, $params, $chain) use (&$conn) {
+ return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn) {
extract($params);
- $result = $conn->query($sql);
- if ( !($result instanceof \SQLite3Result) ) {
+
+ if (!($result = $conn->query($sql)) instanceof SQLite3Result) {
list($code, $error) = $self->error();
- throw new Exception("$sql: $error", $code);
+ throw new QueryException("{$sql}: {$error}", $code);
}
- return $result;
+ return new Result(array('resource' => $result));
});
}
diff --git a/libraries/lithium/data/source/database/adapter/my_sql/Result.php b/libraries/lithium/data/source/database/adapter/my_sql/Result.php
new file mode 100644
index 0000000..74b5c42
--- /dev/null
+++ b/libraries/lithium/data/source/database/adapter/my_sql/Result.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\database\adapter\my_sql;
+
+class Result extends \lithium\data\source\database\Result {
+
+ protected function _next() {
+ return mysql_fetch_row($this->_resource);
+ }
+
+ protected function _close() {
+ if ($this->_resource) {
+ mysql_free_result($this->_resource);
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/database/adapter/sqlite3/Result.php b/libraries/lithium/data/source/database/adapter/sqlite3/Result.php
new file mode 100644
index 0000000..a31b360
--- /dev/null
+++ b/libraries/lithium/data/source/database/adapter/sqlite3/Result.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\database\adapter\sqlite3;
+
+use SQLite3Result;
+
+class Result extends \lithium\data\source\database\Result {
+
+ protected function _next() {
+ if (!$this->_resource instanceof SQLite3Result) {
+ return;
+ }
+ return $this->_resource->fetchArray(SQLITE3_ASSOC);
+ }
+
+ protected function _close() {
+ if (!$this->_resource instanceof SQLite3Result) {
+ return;
+ }
+ $this->_resource->finalize();
+ }
+
+ public function __call($name, $arguments) {
+ if (!$this->_resource instanceof SQLite3Result) {
+ return;
+ }
+
+ if(is_callable(array($this->_resource, $name))) {
+ return call_user_method_array($name, $this->_resource, $arguments);
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/http/adapter/CouchDb.php b/libraries/lithium/data/source/http/adapter/CouchDb.php
index 54bb834..adcc947 100644
--- a/libraries/lithium/data/source/http/adapter/CouchDb.php
+++ b/libraries/lithium/data/source/http/adapter/CouchDb.php
@@ -2,49 +2,71 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\data\source\http\adapter;
-use \Exception;
+use lithium\core\ConfigException;
/**
- * CouchDb adapter
+ * A data source adapter which allows you to connect to Apache CouchDB.
*
+ * By default, it will attempt to connect to the CouchDB running on `localhost` on port
+ * 5984 using HTTP version 1.0.
+ *
+ * @link http://couchdb.apache.org
*/
class CouchDb extends \lithium\data\source\Http {
/**
- * increment value of current result set loop
- * used by `result` to handle rows of json responses
+ * Increment value of current result set loop
+ * used by `result` to handle rows of json responses.
*
* @var string
*/
protected $_iterator = 0;
+
/**
- * True if Database exists
+ * True if Database exists.
*
* @var boolean
*/
protected $_db = false;
+ /**
+ * Classes used by `CouchDb`.
+ *
+ * @var array
+ */
protected $_classes = array(
- 'service' => '\lithium\net\http\Service',
- 'document' => '\lithium\data\collection\Document'
+ 'service' => 'lithium\net\http\Service',
+ 'entity' => 'lithium\data\entity\Document',
+ 'set' => 'lithium\data\collection\DocumentSet',
+ 'array' => 'lithium\data\collection\DocumentArray',
);
+ protected $_handlers = array();
+
/**
- * Constructor
+ * Constructor.
*
* @param array $config
* @return void
*/
public function __construct(array $config = array()) {
- $defaults = array('port' => 5984);
- $config = (array) $config + $defaults;
- parent::__construct($config);
+ $defaults = array('port' => 5984, 'version' => 1);
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+ $this->_handlers += array(
+ 'integer' => function($v) { return (integer) $v; },
+ 'float' => function($v) { return (float) $v; },
+ 'boolean' => function($v) { return (boolean) $v; },
+ );
}
/**
@@ -54,29 +76,36 @@ class CouchDb extends \lithium\data\source\Http {
* @return void
*/
public function __destruct() {
- if ($this->_isConnected) {
- $this->disconnect();
- $this->_db = false;
- unset($this->_connection);
+ if (!$this->_isConnected) {
+ return;
}
+ $this->disconnect();
+ $this->_db = false;
+ unset($this->connection);
}
/**
- * Configures a model class by overriding the default dependencies for `'recordSet'` and
- * `'record'` , and sets the primary key to `'_id'`, in keeping with CouchDb conventions.
+ * Configures a model class by setting the primary key to `'id'`, in keeping with CouchDb
+ * conventions.
*
+ * @see lithium\data\Model::$_meta
+ * @see lithium\data\Model::$_classes
* @param string $class The fully-namespaced model class name to be configured.
* @return Returns an array containing keys `'classes'` and `'meta'`, which will be merged with
* their respective properties in `Model`.
- * @see lithium\data\Model::$_meta
- * @see lithium\data\Model::$_classes
*/
public function configureClass($class) {
- return array('meta' => array('key' => 'id'), 'classes' => array());
+ return array(
+ 'meta' => array('key' => 'id', 'locked' => false),
+ 'schema' => array(
+ 'id' => array('type' => 'string'),
+ 'rev' => array('type' => 'string')
+ )
+ );
}
/**
- * Magic for passing methods to http service
+ * Magic for passing methods to http service.
*
* @param string $method
* @param string $params
@@ -84,30 +113,31 @@ class CouchDb extends \lithium\data\source\Http {
*/
public function __call($method, $params = array()) {
list($path, $data, $options) = ($params + array('/', array(), array()));
- return json_decode($this->_connection->{$method}($path, $data, $options));
+ return json_decode($this->connection->{$method}($path, $data, $options));
}
/**
- * entities
+ * Entities.
*
* @param object $class
* @return void
*/
public function entities($class = null) {
-
}
/**
- * Describe database, create if it does not exist
+ * Describe database, create if it does not exist.
*
* @param string $entity
* @param string $meta
* @return void
*/
- public function describe($entity, $meta = array()) {
+ public function describe($entity, array $meta = array()) {
$database = $this->_config['database'];
+
if (!$this->_db) {
$result = $this->get($database);
+
if (isset($result->db_name)) {
$this->_db = true;
}
@@ -123,9 +153,8 @@ class CouchDb extends \lithium\data\source\Http {
}
}
if (!$this->_db) {
- throw new Exception("{$entity} is not available.");
+ throw new ConfigException("Database `{$entity}` is not available.");
}
- return array('id' => array(), 'rev' => array());
}
/**
@@ -141,7 +170,7 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
- * Create new document
+ * Create new document.
*
* @param string $query
* @param string $options
@@ -151,7 +180,7 @@ class CouchDb extends \lithium\data\source\Http {
$defaults = array('model' => $query->model());
$options += $defaults;
$params = compact('query', 'options');
- $conn =& $this->_connection;
+ $conn =& $this->connection;
$config = $this->_config;
return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
@@ -169,7 +198,7 @@ class CouchDb extends \lithium\data\source\Http {
if (isset($result['_id']) || (isset($result['ok']) && $result['ok'] === true)) {
$result = $self->invokeMethod('_format', array($result, $options));
- $query->record()->update($result['id'], $result);
+ $query->entity()->update($result['id'], $result);
return true;
}
return false;
@@ -177,7 +206,7 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
- * Read from document
+ * Read from document.
*
* @param string $query
* @param string $options
@@ -187,7 +216,7 @@ class CouchDb extends \lithium\data\source\Http {
$defaults = array('return' => 'resource', 'model' => $query->model());
$options += $defaults;
$params = compact('query', 'options');
- $conn =& $this->_connection;
+ $conn =& $this->connection;
$config = $this->_config;
return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
@@ -201,19 +230,30 @@ class CouchDb extends \lithium\data\source\Http {
$_path = '_all_docs';
$conditions['include_docs'] = 'true';
}
- $data = (array) $conditions + (array) $limit + (array) $order;
- $result = json_decode($conn->get("{$config['database']}/{$_path}", $data));
+ $path = "{$config['database']}/{$_path}";
+
+ $args = (array) $conditions + (array) $limit + (array) $order;
+ $result = (array) json_decode($conn->get($path, $args), true);
+
+ $data = $stats = array();
+
+ if (isset($result['_id'])) {
+ $data = array($result);
+ } elseif (isset($result['rows'])) {
+ $data = array_map(function($row) { return $row['value']; }, $result['rows']);
- if (isset($result->error) && $result->error == 'not_found') {
- return $result;
+ unset($result['rows']);
+ $stats = $result;
}
- $options += compact('result');
- return $self->invokeMethod('_result', array('document', $query, $options));
+
+ $stats += array('total_rows' => null, 'offset' => null);
+ $opts = compact('stats') + array('class' => 'set', 'exists' => true);
+ return $self->item($query->model(), $data, $opts);
});
}
/**
- * Update document
+ * Update document.
*
* @param string $query
* @param string $options
@@ -221,17 +261,18 @@ class CouchDb extends \lithium\data\source\Http {
*/
public function update($query, array $options = array()) {
$params = compact('query', 'options');
- $conn =& $this->_connection;
+ $conn =& $this->connection;
$config = $this->_config;
return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
$query = $params['query'];
$options = $params['options'];
- $data = $query->data();
$params = $query->export($self);
extract($params, EXTR_OVERWRITE);
list($_path, $conditions) = (array) $conditions;
+ $data = $query->data();
+
foreach (array('id', 'rev') as $key) {
$data["_{$key}"] = isset($data[$key]) ? (string) $data[$key] : null;
unset($data[$key]);
@@ -242,18 +283,18 @@ class CouchDb extends \lithium\data\source\Http {
if (isset($result['_id']) || (isset($result['ok']) && $result['ok'] === true)) {
$result = $self->invokeMethod('_format', array($result, $options));
- $query->record()->update($result['id'], $result);
+ $query->entity()->update($result['id'], array('rev' => $result['rev']));
return true;
}
- if (isset($result['error']) && $result['error'] === 'conflict') {
- return $self->read($query, $options);
+ if (isset($result['error'])) {
+ $query->entity()->errors(array($result['error']));
}
return false;
});
}
/**
- * Delete document
+ * Delete document.
*
* @param string $query
* @param string $options
@@ -261,7 +302,7 @@ class CouchDb extends \lithium\data\source\Http {
*/
public function delete($query, array $options = array()) {
$params = compact('query', 'options');
- $conn =& $this->_connection;
+ $conn =& $this->connection;
$config = $this->_config;
return $this->_filter(__METHOD__, $params, function($self, $params) use (&$conn, $config) {
@@ -279,6 +320,24 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
+ * Executes calculation-related queries, such as those required for `count`.
+ *
+ * @param string $type Only accepts `count`.
+ * @param mixed $query The query to be executed.
+ * @param array $options Optional arguments for the `read()` query that will be executed
+ * to obtain the calculation result.
+ * @return integer Result of the calculation.
+ */
+ public function calculation($type, $query, array $options = array()) {
+ switch ($type) {
+ case 'count':
+ return $this->read($query, $options)->stats('total_rows');
+ default:
+ return null;
+ }
+ }
+
+ /**
* Returns a newly-created `Document` object, bound to a model and populated with default data
* and options.
*
@@ -290,13 +349,27 @@ class CouchDb extends \lithium\data\source\Http {
* in `$model`.
*/
public function item($model, array $data = array(), array $options = array()) {
- $result = $data = $this->_format($data);
- $class = $this->_classes['document'];
- return new $class(compact('model', 'data') + $options);
+ return parent::item($model, $this->_format($data), $options);
+ }
+
+ public function cast($entity, array $data, array $options = array()) {
+ $defaults = array('pathKey' => null, 'model' => null);
+ $options += $defaults;
+ $model = $options['model'] ?: $entity->model();
+
+ foreach ($data as $key => $val) {
+ if (!is_array($val)) {
+ continue;
+ }
+ $pathKey = $options['pathKey'] ? "{$options['pathKey']}.{$key}" : $key;
+ $class = (range(0, count($val) - 1) === array_keys($val)) ? 'array' : 'entity';
+ $data[$key] = $this->item($model, $val, compact('class', 'pathKey') + $options);
+ }
+ return parent::cast($entity, $data, $options);
}
/**
- * get result
+ * Get result.
*
* @param string $type
* @param string $resource
@@ -330,7 +403,7 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
- * handle conditions
+ * Handle conditions.
*
* @param string $conditions
* @param string $context
@@ -359,7 +432,7 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
- * fields for query
+ * Fields for query.
*
* @param string $fields
* @param string $context
@@ -370,7 +443,7 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
- * limit for query
+ * Limit for query.
*
* @param string $limit
* @param string $context
@@ -381,7 +454,7 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
- * order for query
+ * Order for query.
*
* @param string $order
* @param string $context
@@ -392,7 +465,29 @@ class CouchDb extends \lithium\data\source\Http {
}
/**
- * Formats a CouchDb result set into a standard result to be passed to item
+ * With no parameter, always returns `true`, since CouchDB only depends on HTTP. With a
+ * parameter, queries for a specific supported feature.
+ *
+ * @param string $feature Test for support for a specific feature, i.e. `"transactions"` or
+ * `"arrays"`.
+ * @return boolean Returns `true` if the particular feature support is enabled, otherwise
+ * `false`.
+ */
+ public static function enabled($feature = null) {
+ if (!$feature) {
+ return true;
+ }
+ $features = array(
+ 'arrays' => true,
+ 'transactions' => false,
+ 'booleans' => true,
+ 'relationships' => false,
+ );
+ return isset($features[$feature]) ? $features[$feature] : null;
+ }
+
+ /**
+ * Formats a CouchDb result set into a standard result to be passed to item.
*
* @param string $data data returned from query
* @param string $options
@@ -408,21 +503,6 @@ class CouchDb extends \lithium\data\source\Http {
unset($data['_id'], $data['_rev']);
return $data;
}
-
- /**
- * Handle the result from read
- *
- * @param string $type
- * @param string $query
- * @param string $config
- * @return void
- */
- protected function _result($type, $query, $config = array()) {
- $defaults = array('handle' => &$this, 'exists' => true);
- $config = compact('query') + $config + $defaults;
- $class = $this->_classes[$type];
- return new $class($config);
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/mongo_db/Exporter.php b/libraries/lithium/data/source/mongo_db/Exporter.php
new file mode 100644
index 0000000..736212a
--- /dev/null
+++ b/libraries/lithium/data/source/mongo_db/Exporter.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\mongo_db;
+
+use lithium\util\Set;
+
+class Exporter extends \lithium\core\StaticObject {
+
+ protected static $_map = array(
+ 'MongoId' => 'id',
+ 'MongoDate' => 'date',
+ 'MongoCode' => 'code',
+ 'MongoBinData' => 'binary',
+ 'datetime' => 'date',
+ 'timestamp' => 'date',
+ 'int' => 'integer',
+ );
+
+ public static function get($type, $export, array $options = array()) {
+ $defaults = array('whitelist' => array());
+ $options += $defaults;
+
+ if (!method_exists(get_called_class(), $method = "_{$type}") || !$export) {
+ return;
+ }
+ return static::$method($export, array('finalize' => true) + $options);
+ }
+
+ public static function cast($data, $schema, $database, array $options = array()) {
+ $defaults = array(
+ 'pathKey' => null, 'handlers' => array(), 'model' => null, 'arrays' => true
+ );
+ $options += $defaults;
+ $typeMap = static::$_map;
+
+ foreach ($data as $key => $value) {
+ if (is_object($value)) {
+ continue;
+ }
+ $path = $options['pathKey'] ? "{$options['pathKey']}.{$key}" : $key;
+ $field = (isset($schema[$path]) ? $schema[$path] : array());
+ $field += array('type' => null, 'array' => null);
+ $type = isset($typeMap[$field['type']]) ? $typeMap[$field['type']] : $field['type'];
+
+ $isObject = ($type == 'object');
+ $isArray = (is_array($value) && $field['array'] !== false && !$isObject);
+ $isArray = $field['array'] || $isArray;
+
+ if (isset($options['handlers'][$type]) && $handler = $options['handlers'][$type]) {
+ $value = $isArray ? array_map($handler, (array) $value) : $handler($value);
+ }
+ if (!$options['arrays']) {
+ $data[$key] = $value;
+ continue;
+ }
+ $pathKey = $path;
+
+ if (!is_array($value) && !$field['array']) {
+ $data[$key] = $value;
+ continue;
+ }
+
+ if ($field['array']) {
+ $opts = array('class' => 'array') + $options;
+ $value = ($value === null) ? array() : $value;
+ $value = is_array($value) ? $value : array($value);
+ } elseif (is_array($value)) {
+ $arrayType = !$isObject && (array_keys($value) === range(0, count($value) - 1));
+ $opts = $arrayType ? array('class' => 'array') + $options : $options;
+ }
+ $data[$key] = $database->item($options['model'], $value, compact('pathKey') + $opts);
+ }
+ return $data;
+ }
+
+ public static function toCommand($changes) {
+ $map = array(
+ 'create' => null,
+ 'update' => '$set',
+ 'increment' => '$inc',
+ 'remove' => '$unset',
+ 'rename' => '$rename',
+ );
+ $result = array();
+
+ foreach ($map as $from => $to) {
+ if (!isset($changes[$from])) {
+ continue;
+ }
+ if (!$to) {
+ $result = array_merge($result, $changes[$from]);
+ }
+ $result[$to] = $changes[$from];
+ }
+ unset($result['$set']['_id']);
+ return $result;
+ }
+
+ protected static function _create($export, array $options) {
+ $export += array('data' => array(), 'update' => array(), 'remove' => array(), 'key' => '');
+ $data = array_merge($export['data'], $export['update']);
+ $data = array_diff_key($data, $export['remove']);
+
+ $result = array('create' => array());
+ $localOpts = array('finalize' => false) + $options;
+
+ foreach ($data as $key => $val) {
+ if (is_object($val) && method_exists($val, 'export')) {
+ $data[$key] = static::_create($val->export($options), $localOpts);
+ }
+ }
+ return ($options['finalize']) ? array('create' => $data) : $data;
+ }
+
+ protected static function _update($export) {
+ $export += array('update' => array(), 'remove' => array(), 'key' => '');
+ $path = $export['key'] ? "{$export['key']}." : "";
+ $data = $export['update'];
+ $result = array();
+
+ if (!$export['exists']) {
+ $data = array_merge($export['data'], $data);
+ }
+ $data = array_diff_key($data, $export['remove']);
+ $nested = array_diff_key($export['data'], $data);
+
+ foreach ($export['remove'] as $key => $val) {
+ $result['remove']["{$path}{$key}"] = $val;
+ }
+
+ foreach ($data as $key => $val) {
+ if (is_object($val) && method_exists($val, 'export')) {
+ $result = static::_appendObject($result, $path, $key, $val);
+ continue;
+ }
+ $result['update']["{$path}{$key}"] = $val;
+ }
+
+ foreach (array_diff_key($nested, $export['remove']) as $key => $val) {
+ if (is_object($val) && method_exists($val, 'export')) {
+ $result = static::_appendObject($result, $path, $key, $val);
+ }
+ }
+ return $result;
+ }
+
+ protected static function _appendObject($changes, $path, $key, $object) {
+ $options = array('finalize' => false);
+
+ if ($object->exists()) {
+ return Set::merge($changes, static::_update($object->export()));
+ }
+ $changes['update']["{$path}{$key}"] = static::_create($object->export(), $options);
+ return $changes;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/mongo_db/Result.php b/libraries/lithium/data/source/mongo_db/Result.php
new file mode 100644
index 0000000..a6f86ed
--- /dev/null
+++ b/libraries/lithium/data/source/mongo_db/Result.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\mongo_db;
+
+use MongoGridFSFile;
+
+class Result extends \lithium\core\Object implements \Iterator {
+
+ protected $_iterator = 0;
+
+ protected $_current = null;
+
+ protected $_resource = null;
+
+ protected $_autoConfig = array('resource');
+
+ public function __construct(array $config = array()) {
+ $defaults = array('resource' => null);
+ parent::__construct($config + $defaults);
+ }
+
+ public function resource() {
+ return $this->_resource;
+ }
+
+ public function rewind() {
+ return null;
+ }
+
+ public function valid() {
+ return !empty($this->_resource);
+ }
+
+ public function current() {
+ return $this->_current;
+ }
+
+ public function key() {
+ return $this->_iterator;
+ }
+
+ public function next() {
+ if ($this->_resource->hasNext()) {
+ $result = $this->_resource->getNext();
+ $isFile = ($result instanceof MongoGridFSFile);
+ return $isFile ? array('file' => $result) + $result->file : $result;
+ }
+ unset($this->_resource);
+ $this->_resource = null;
+ }
+
+ public function __destruct() {
+ unset($this->_resource);
+ $this->_resource = null;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/Catalog.php b/libraries/lithium/g11n/Catalog.php
index 37280ce..00998d2 100644
--- a/libraries/lithium/g11n/Catalog.php
+++ b/libraries/lithium/g11n/Catalog.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\g11n;
-use \lithium\core\Libraries;
-use \lithium\util\Collection;
+use lithium\core\Libraries;
+use lithium\util\Collection;
/**
* Globalization data is not just translated messages, it is validation rules, formats and a lot
@@ -36,6 +36,12 @@ class Catalog extends \lithium\core\Adaptable {
*/
protected static $_adapters = 'adapter.g11n.catalog';
+ /**
+ * Sets configurations for this Adaptable implementation.
+ *
+ * @param array $config Configurations, indexed by name.
+ * @return object|void `Collection` of configurations or void if setting configurations.
+ */
public static function config($config = null) {
$defaults = array('scope' => null);
@@ -58,26 +64,28 @@ class Catalog extends \lithium\core\Adaptable {
*
* Usage:
* {{{
- * Catalog::read('message', 'zh');
- * Catalog::read('validation.postalCode', 'en_US');
+ * Catalog::read(true, 'message', 'zh');
+ * Catalog::read('default', 'message', 'zh');
+ * Catalog::read('default', 'validation.postalCode', 'en_US');
* }}}
*
+ * @param mixed $name Provide a single configuration name as a string or multiple ones as
+ * an array which will be used to read from. Pass `true` to use all configurations.
* @param string $category A (dot-delimeted) category.
* @param string $locale A locale identifier.
* @param array $options Valid options are:
- * - `'name'`: One or multiple configuration names.
* - `'scope'`: The scope to use.
* - `'lossy'`: Whether or not to use the compact and lossy format, defaults to `true`.
* @return array|void If available the requested data, else `null`.
*/
- public static function read($category, $locale, array $options = array()) {
- $defaults = array('name' => null, 'scope' => null, 'lossy' => true);
+ public static function read($name, $category, $locale, array $options = array()) {
+ $defaults = array('scope' => null, 'lossy' => true);
$options += $defaults;
$category = strtok($category, '.');
$id = strtok('.');
- $names = (array) $options['name'] ?: array_keys(static::$_configurations);
+ $names = $name === true ? array_keys(static::$_configurations) : (array) $name;
$results = array();
foreach (Locale::cascade($locale) as $cascaded) {
@@ -109,19 +117,19 @@ class Catalog extends \lithium\core\Adaptable {
* $data = array(
* 'color' => '色'
* );
- * Catalog::write('message', 'ja', $data, array('name' => 'runtime'));
+ * Catalog::write('runtime', 'message', 'ja', $data);
* }}}
*
+ * @param string $name Provide a configuration name to use for writing.
* @param string $category A (dot-delimited) category.
* @param string $locale A locale identifier.
* @param mixed $data If method is used without specifying an id must be an array.
* @param array $options Valid options are:
- * - `'name'`: One or multiple configuration names.
* - `'scope'`: The scope to use.
* @return boolean Success.
*/
- public static function write($category, $locale, $data, array $options = array()) {
- $defaults = array('name' => null, 'scope' => null);
+ public static function write($name, $category, $locale, $data, array $options = array()) {
+ $defaults = array('scope' => null);
$options += $defaults;
$category = strtok($category, '.');
@@ -137,13 +145,8 @@ class Catalog extends \lithium\core\Adaptable {
}
});
- $names = (array) $options['name'] ?: array_keys(static::$_configurations);
-
- foreach ($names as $name) {
- $adapter = static::adapter($name);
- return $adapter->write($category, $locale, $options['scope'], $data);
- }
- return false;
+ $adapter = static::adapter($name);
+ return $adapter->write($category, $locale, $options['scope'], $data);
}
}
diff --git a/libraries/lithium/g11n/Locale.php b/libraries/lithium/g11n/Locale.php
index 17bb845..8e9bbe9 100644
--- a/libraries/lithium/g11n/Locale.php
+++ b/libraries/lithium/g11n/Locale.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\g11n;
-use \BadMethodCallException;
-use \InvalidArgumentException;
+use BadMethodCallException;
+use InvalidArgumentException;
/**
* The `Locale` class provides methods to deal with locale identifiers. The locale
@@ -70,7 +70,7 @@ class Locale extends \lithium\core\StaticObject {
$tags = static::invokeMethod('decompose', $params);
if (!isset(static::$_tags[$method])) {
- throw new BadMethodCallException("Invalid locale tag `{$method}`");
+ throw new BadMethodCallException("Invalid locale tag `{$method}`.");
}
return isset($tags[$method]) ? $tags[$method] : null;
}
@@ -109,7 +109,7 @@ class Locale extends \lithium\core\StaticObject {
$regex .= '(?:[_-](?P<variant>[a-z]{5,}))?';
if (!preg_match("/^{$regex}$/i", $locale, $matches)) {
- throw new InvalidArgumentException("Locale `{$locale}` could not be parsed");
+ throw new InvalidArgumentException("Locale `{$locale}` could not be parsed.");
}
return array_filter(array_intersect_key($matches, static::$_tags));
}
diff --git a/libraries/lithium/g11n/Message.php b/libraries/lithium/g11n/Message.php
index 75cb728..47c4970 100644
--- a/libraries/lithium/g11n/Message.php
+++ b/libraries/lithium/g11n/Message.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\g11n;
-use \lithium\core\Environment;
-use \lithium\util\String;
-use \lithium\g11n\Locale;
-use \lithium\g11n\Catalog;
+use lithium\core\Environment;
+use lithium\util\String;
+use lithium\g11n\Locale;
+use lithium\g11n\Catalog;
/**
* The `Message` class is concerned with an aspect of globalizing static message strings
@@ -48,6 +48,15 @@ use \lithium\g11n\Catalog;
class Message extends \lithium\core\StaticObject {
/**
+ * Holds cached message pages generated and used
+ * by `lithium\g11n\Message::_translated()`.
+ *
+ * @var array
+ * @see lithium\g11n\Message::_translated()
+ */
+ protected static $_cachedPages = array();
+
+ /**
* Translates a message according to the current or provided locale
* and into it's correct plural form.
*
@@ -92,7 +101,7 @@ class Message extends \lithium\core\StaticObject {
'locale' => Environment::get('locale'),
'scope' => null,
'default' => null,
- 'noop' => false
+ 'noop' => false,
);
extract($options + $defaults);
@@ -142,6 +151,23 @@ class Message extends \lithium\core\StaticObject {
}
/**
+ * Returns or sets the page cache used for mapping message ids to translations.
+ *
+ * @param array $cache A multidimensional array to use when pre-populating the cache. The
+ * structure of the array is `scope/locale/id`. If `false`, the cache is cleared.
+ * @return array Returns an array of cached pages, formatted per the description for `$cache`.
+ */
+ public static function cache($cache = null) {
+ if ($cache === false) {
+ static::$_cachedPages = array();
+ }
+ if (is_array($cache)) {
+ static::$_cachedPages += $cache;
+ }
+ return static::$_cachedPages;
+ }
+
+ /**
* Retrieves translations through the `Catalog` class by using `$id` as the lookup
* key and taking the current or - if specified - the provided locale as well as the
* scope into account. Hereupon the correct plural form is determined by passing the
@@ -156,15 +182,20 @@ class Message extends \lithium\core\StaticObject {
* @return string|void The translation or `null` if none could be found or the plural
* form could not be determined.
* @filter
- * @todo Message pages need caching.
*/
protected static function _translated($id, $count, $locale, array $options = array()) {
$params = compact('id', 'count', 'locale', 'options');
- return static::_filter(__METHOD__, $params, function($self, $params, $chain) {
+ $cache =& static::$_cachedPages;
+ return static::_filter(__FUNCTION__, $params, function($self, $params) use (&$cache) {
extract($params);
- $page = Catalog::read('message', $locale, $options);
+ if (!isset($cache[$options['scope']][$locale])) {
+ $cache[$options['scope']][$locale] = Catalog::read(
+ true, 'message', $locale, $options
+ );
+ }
+ $page = $cache[$options['scope']][$locale];
if (!isset($page[$id])) {
return null;
@@ -173,10 +204,10 @@ class Message extends \lithium\core\StaticObject {
return $page[$id];
}
- if (!isset($page['plural']) || !is_callable($page['plural'])) {
+ if (!isset($page['pluralRule']) || !is_callable($page['pluralRule'])) {
return null;
}
- $key = $page['plural']($count);
+ $key = $page['pluralRule']($count);
if (isset($page[$id][$key])) {
return $page[$id][$key];
diff --git a/libraries/lithium/g11n/catalog/Adapter.php b/libraries/lithium/g11n/catalog/Adapter.php
index 0540858..ac66d14 100644
--- a/libraries/lithium/g11n/catalog/Adapter.php
+++ b/libraries/lithium/g11n/catalog/Adapter.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/catalog/adapter/Cldr.php b/libraries/lithium/g11n/catalog/adapter/Cldr.php
deleted file mode 100644
index 4259ed2..0000000
--- a/libraries/lithium/g11n/catalog/adapter/Cldr.php
+++ /dev/null
@@ -1,169 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\g11n\catalog\adapter;
-
-use \Exception;
-use \SimpleXmlElement;
-use \lithium\util\Inflector;
-use \lithium\g11n\Locale;
-
-/**
- * The `Cldr` class is an adapter which allows reading from the Common Locale Data Repository
- * maintained by the Unicode Consortium.
- *
- * The directory the `'path'` configuration setting is pointing to should contain the contents
- * from the CLDR as distributed with i.e. `core.zip` which can be downloaded from the unicode site.
- *
- * The directory as configured by the `'path'` setting which equals the `commons` directory
- * from `core.zip` should be structured according to the following example.
- *
- * {{{
- * | - `main`
- * | - `supplemental`
- * | - ...
- * }}}
- *
- * @link http://unicode.org/cldr
- * @link http://unicode.org/Public/cldr/1.7.0/core.zip
- */
-class Cldr extends \lithium\g11n\catalog\Adapter {
-
- /**
- * Constructor.
- *
- * @param array $config Available configuration options are:
- * - `'path'`: The path to the directory holding the data.
- * - `'scope'`: Scope to use.
- * @return object
- */
- public function __construct(array $config = array()) {
- $defaults = array('path' => null, 'scope' => null);
- parent::__construct($config + $defaults);
- }
-
- /**
- * Initializer. Checks if the configured path exists.
- *
- * @return void
- * @throws \Exception
- */
- protected function _init() {
- parent::_init();
- if (!is_dir($this->_config['path'])) {
- throw new Exception("Cldr directory does not exist at `{$this->_config['path']}`");
- }
- }
-
- /**
- * Reads data.
- *
- * @param string $category A category. The following categories are supported:
- * - `'currency'`
- * - `'language'`
- * - `'script'`
- * - `'territory'`
- * - `'validation'`
- * @param string $locale A locale identifier.
- * @param string $scope The scope for the current operation.
- * @return array|void
- */
- public function read($category, $locale, $scope) {
- if ($scope != $this->_config['scope']) {
- return null;
- }
- $path = $this->_config['path'];
-
- switch ($category) {
- case 'currency':
- $data = $this->_readCurrency($path, $locale);
- break;
- case 'language':
- case 'script':
- case 'territory':
- $data = $this->_readList($path, $category, $locale);
- break;
- case 'validation':
- $data = $this->_readValidation($path, $locale);
- break;
- default:
- return null;
- }
- return $data;
- }
-
- protected function _readValidation($path, $locale) {
- if (!$territory = Locale::territory($locale)) {
- return null;
- }
- $data = array();
-
- $file = "{$path}/supplemental/postalCodeData.xml";
- $query = "/supplementalData/postalCodeData";
- $query .= "/postCodeRegex[@territoryId=\"{$territory}\"]";
-
- $nodes = $this->_parseXml($file, $query);
- $regex = (string) current($nodes);
-
- return $this->_merge($data, array(
- 'id' => 'postalCode',
- 'translated' => "/^{$regex}$/"
- ));
- }
-
- protected function _readList($path, $category, $locale) {
- $plural = Inflector::pluralize($category);
-
- $file = "{$path}/main/{$locale}.xml";
- $query = "/ldml/localeDisplayNames/{$plural}/{$category}";
-
- $nodes = $this->_parseXml($file, $query);
- $data = array();
-
- foreach ($nodes as $node) {
- $data = $this->_merge($data, array(
- 'id' => (string) $node['type'],
- 'translated' => (string) $node
- ));
- }
- return $data;
- }
-
- protected function _readCurrency($path, $locale) {
- $file = "{$path}/main/{$locale}.xml";
- $query = "/ldml/numbers/currencies/currency";
-
- $nodes = $this->_parseXml($file, $query);
- $data = array();
-
- foreach ($nodes as $node) {
- $displayNames = $node->xpath('displayName');
-
- $data = $this->_merge($data, array(
- 'id' => (string) $node['type'],
- 'translated' => (string) current($displayNames)
- ));
- }
- return $data;
- }
-
- /**
- * Parses a XML file and retrieves data from it using an XPATH query
- * and a given closure.
- *
- * @param string $file Absolute path to the XML file.
- * @param string $query An XPATH query to select items.
- * @return array
- */
- protected function _parseXml($file, $query) {
- $document = new SimpleXmlElement($file, LIBXML_COMPACT, true);
- return $document->xpath($query);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/catalog/adapter/Code.php b/libraries/lithium/g11n/catalog/adapter/Code.php
index 83065ef..ac02856 100644
--- a/libraries/lithium/g11n/catalog/adapter/Code.php
+++ b/libraries/lithium/g11n/catalog/adapter/Code.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\g11n\catalog\adapter;
-use \Exception;
-use \RecursiveIteratorIterator;
-use \RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RecursiveDirectoryIterator;
+use lithium\core\ConfigException;
+use lithium\template\view\Compiler;
/**
* The `Code` class is an adapter which treats files containing code as just another source
@@ -47,7 +48,8 @@ class Code extends \lithium\g11n\catalog\Adapter {
protected function _init() {
parent::_init();
if (!is_dir($this->_config['path'])) {
- throw new Exception("Code directory does not exist at `{$this->_config['path']}`");
+ $message = "Code directory does not exist at path `{$this->_config['path']}`.";
+ throw new ConfigException($message);
}
}
@@ -109,6 +111,7 @@ class Code extends \lithium\g11n\catalog\Adapter {
*/
protected function _parsePhp($file) {
$contents = file_get_contents($file);
+ $contents = Compiler::compile($contents);
$defaults = array(
'ids' => array(),
@@ -134,7 +137,7 @@ class Code extends \lithium\g11n\catalog\Adapter {
if ($open) {
if ($position >= ($open === 'singular' ? 1 : 2)) {
$data = $this->_merge($data, array(
- 'id' => &$ids['singular'],
+ 'id' => $ids['singular'],
'ids' => $ids,
'occurrences' => array($occurrence),
));
@@ -169,9 +172,19 @@ class Code extends \lithium\g11n\catalog\Adapter {
* @return array The merged data.
*/
protected function _merge(array $data, array $item) {
- array_walk($item['ids'], function(&$value) {
- $value = substr($value, 1, -1);
- });
+ $filter = function ($value) use (&$filter) {
+ if (is_array($value)) {
+ return array_map($filter, $value);
+ }
+ return substr($value, 1, -1);
+ };
+ $fields = array('id', 'ids', 'translated');
+
+ foreach ($fields as $field) {
+ if (isset($item[$field])) {
+ $item[$field] = $filter($item[$field]);
+ }
+ }
return parent::_merge($data, $item);
}
}
diff --git a/libraries/lithium/g11n/catalog/adapter/Gettext.php b/libraries/lithium/g11n/catalog/adapter/Gettext.php
index c81c5f4..e7a1b40 100644
--- a/libraries/lithium/g11n/catalog/adapter/Gettext.php
+++ b/libraries/lithium/g11n/catalog/adapter/Gettext.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\g11n\catalog\adapter;
-use \Exception;
+use RangeException;
+use lithium\core\ConfigException;
/**
* The `Gettext` class is an adapter for reading and writing PO and MO files without the
@@ -39,8 +40,8 @@ use \Exception;
* }}}
*
* @see lithium\g11n\Locale
- * @link http://php.net/setlocale
- * @link http://www.gnu.org/software/gettext/manual/gettext.html
+ * @link http://php.net/setlocale PHP Manual: setlocale()
+ * @link http://www.gnu.org/software/gettext/manual/gettext.html GNU Gettext Utilities
*/
class Gettext extends \lithium\g11n\catalog\Adapter {
@@ -51,7 +52,7 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
* @see lithium\g11n\catalog\adapter\Gettext::_parseMo()
* @var float
*/
- const MO_LE_MAGIC = 0x950412de;
+ const MO_LITTLE_ENDIAN_MAGIC = 0x950412de;
/**
* Magic used for validating the format of a MO file as well as
@@ -60,7 +61,7 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
* @see lithium\g11n\catalog\adapter\Gettext::_parseMo()
* @var float
*/
- const MO_BE_MAGIC = 0xde120495;
+ const MO_BIG_ENDIAN_MAGIC = 0xde120495;
/**
* The size of the header of a MO file in bytes.
@@ -86,12 +87,13 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
* Initializer. Checks if the configured path exists.
*
* @return void
- * @throws \Exception
+ * @throws ConfigException
*/
protected function _init() {
parent::_init();
if (!is_dir($this->_config['path'])) {
- throw new Exception("Gettext directory does not exist at `{$this->_config['path']}`");
+ $message = "Gettext directory does not exist at path `{$this->_config['path']}`.";
+ throw new ConfigException($message);
}
}
@@ -160,10 +162,7 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
if (($pos = strpos($category, 'Template')) !== false) {
$category = substr($category, 0, $pos);
-
- return array(
- "{$path}/{$category}_{$scope}.pot"
- );
+ return array("{$path}/{$category}_{$scope}.pot");
}
if ($category == 'message') {
@@ -261,23 +260,23 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
*
* @param resource $stream
* @return array
- * @throws Exception If stream content has an invalid format.
+ * @throws RangeException If stream content has an invalid format.
*/
protected function _parseMo($stream) {
$stat = fstat($stream);
if ($stat['size'] < self::MO_HEADER_SIZE) {
- throw new Exception("MO stream caontent has an invalid format");
+ throw new RangeException("MO stream content has an invalid format.");
}
$magic = unpack('V1', fread($stream, 4));
$magic = hexdec(substr(dechex(current($magic)), -8));
- if ($magic == self::MO_LE_MAGIC) {
+ if ($magic == self::MO_LITTLE_ENDIAN_MAGIC) {
$isBigEndian = false;
- } elseif ($magic == self::MO_BE_MAGIC) {
+ } elseif ($magic == self::MO_BIG_ENDIAN_MAGIC) {
$isBigEndian = true;
} else {
- throw new Exception("MO stream content has an invalid format");
+ throw new RangeException("MO stream content has an invalid format.");
}
$header = array(
@@ -286,7 +285,7 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
'offsetId' => null,
'offsetTranslated' => null,
'sizeHashes' => null,
- 'offsetHashes' => null
+ 'offsetHashes' => null,
);
foreach ($header as &$value) {
$value = $this->_readLong($stream, $isBigEndian);
@@ -325,10 +324,8 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
$translated = explode("\000", $translated);
}
- $data = $this->_merge($data, array(
- 'ids' => array('singular' => $singularId, 'plural' => $pluralId),
- 'translated' => $translated
- ));
+ $ids = array('singular' => $singularId, 'plural' => $pluralId);
+ $data = $this->_merge($data, compact('ids', 'translated'));
}
return $data;
}
@@ -351,7 +348,7 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
*
* To improve portability accross libraries the header is generated according
* to the format of the output of `xgettext`. This means using the same names for
- * placeholders as well as including an empty fuzzy entry. The empty entry at the
+ * placeholders as well as including an empty entry. The empty entry at the
* beginning aids in parsing the file as it _attracts_ the preceding comments and
* following metadata when parsed which could otherwise be mistaken as a continued
* translation. The only difference in the header format is the initial header which
@@ -364,7 +361,6 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
protected function _compilePo($stream, array $data) {
$output[] = '# This file is distributed under the same license as the PACKAGE package.';
$output[] = '#';
- $output[] = '#, fuzzy';
$output[] = 'msgid ""';
$output[] = 'msgstr ""';
$output[] = '"Project-Id-Version: PACKAGE VERSION\n"';
@@ -373,7 +369,7 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
$output[] = '"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"';
$output[] = '"Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"';
$output[] = '"MIME-Version: 1.0\n"';
- $output[] = '"Content-Type: text/plain; charset=CHARSET\n"';
+ $output[] = '"Content-Type: text/plain; charset=UTF-8\n"';
$output[] = '"Content-Transfer-Encoding: 8bit\n"';
$output[] = '"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"';
$output[] = '';
@@ -470,6 +466,11 @@ class Gettext extends \lithium\g11n\catalog\Adapter {
if (!isset($item['ids']['singular'])) {
$item['ids']['singular'] = $item['id'];
}
+ if (isset($item['occurrences'])) {
+ foreach ($item['occurrences'] as &$occurrence) {
+ $occurrence['file'] = str_replace(LITHIUM_APP_PATH, '', $occurrence['file']);
+ }
+ }
return parent::_prepareForWrite($item);
}
diff --git a/libraries/lithium/g11n/catalog/adapter/Memory.php b/libraries/lithium/g11n/catalog/adapter/Memory.php
index 855c11c..efca6df 100644
--- a/libraries/lithium/g11n/catalog/adapter/Memory.php
+++ b/libraries/lithium/g11n/catalog/adapter/Memory.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/catalog/adapter/Php.php b/libraries/lithium/g11n/catalog/adapter/Php.php
index 1c0be70..588f613 100644
--- a/libraries/lithium/g11n/catalog/adapter/Php.php
+++ b/libraries/lithium/g11n/catalog/adapter/Php.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\g11n\catalog\adapter;
-use \Exception;
+use lithium\core\ConfigException;
/**
* The `Php` class is an adapter for reading from PHP files which hold g11n data
@@ -25,7 +25,7 @@ use \Exception;
* );
* ?>
* }}}
-
+ *
* The adapter works with a directory structure below. The example shows the structure
* for the directory as given by the `'path'` configuration setting. It is similar to
* the one used by the the `Gettext` adapter.
@@ -71,10 +71,19 @@ class Php extends \lithium\g11n\catalog\Adapter {
protected function _init() {
parent::_init();
if (!is_dir($this->_config['path'])) {
- throw new Exception("Php directory does not exist at `{$this->_config['path']}`");
+ $message = "Php directory does not exist at path `{$this->_config['path']}`.";
+ throw new ConfigException($message);
}
}
+ /**
+ * Reads data.
+ *
+ * @param string $category A category.
+ * @param string $locale A locale identifier.
+ * @param string $scope The scope for the current operation.
+ * @return array|void
+ */
public function read($category, $locale, $scope) {
$path = $this->_config['path'];
$file = $this->_file($category, $locale, $scope);
@@ -88,6 +97,14 @@ class Php extends \lithium\g11n\catalog\Adapter {
return $data;
}
+ /**
+ * Helper method for transforming a category, locale and scope into a filename.
+ *
+ * @param string $category Category name.
+ * @param string $locale Locale identifier.
+ * @param string $scope Current operation scope.
+ * @return string Filename.
+ */
protected function _file($category, $locale, $scope) {
$path = $this->_config['path'];
$scope = $scope ?: 'default';
diff --git a/libraries/lithium/g11n/resources/php/da_DK/validation/default.php b/libraries/lithium/g11n/resources/php/da_DK/validation/default.php
index 549b84a..51aa32e 100644
--- a/libraries/lithium/g11n/resources/php/da_DK/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/da_DK/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/de/message/default.php b/libraries/lithium/g11n/resources/php/de/message/default.php
new file mode 100644
index 0000000..ffb813a
--- /dev/null
+++ b/libraries/lithium/g11n/resources/php/de/message/default.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * Message data for `de`.
+ *
+ * Plural rule and forms derived from the GNU gettext documentation.
+ *
+ * @link http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
+ */
+return array(
+ 'pluralForms' => 2,
+ 'pluralRule' => function ($n) { return $n == 1 ? 0 : 1; }
+);
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/resources/php/de_BE/validation/default.php b/libraries/lithium/g11n/resources/php/de_BE/validation/default.php
index 885e31b..fe900fa 100644
--- a/libraries/lithium/g11n/resources/php/de_BE/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/de_BE/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/de_DE/validation/default.php b/libraries/lithium/g11n/resources/php/de_DE/validation/default.php
index 889f4a0..2f4f190 100644
--- a/libraries/lithium/g11n/resources/php/de_DE/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/de_DE/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/en/message/default.php b/libraries/lithium/g11n/resources/php/en/message/default.php
new file mode 100644
index 0000000..c0d0148
--- /dev/null
+++ b/libraries/lithium/g11n/resources/php/en/message/default.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * Message data for `en`.
+ *
+ * Plural rule and forms derived from the GNU gettext documentation.
+ *
+ * @link http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
+ */
+return array(
+ 'pluralForms' => 2,
+ 'pluralRule' => function ($n) { return $n != 1 ? 1 : 0; }
+);
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/resources/php/en_CA/validation/default.php b/libraries/lithium/g11n/resources/php/en_CA/validation/default.php
index 5ef8f85..12c865d 100644
--- a/libraries/lithium/g11n/resources/php/en_CA/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/en_CA/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/en_GB/validation/default.php b/libraries/lithium/g11n/resources/php/en_GB/validation/default.php
index f69e56e..9f21a99 100644
--- a/libraries/lithium/g11n/resources/php/en_GB/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/en_GB/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/en_US/validation/default.php b/libraries/lithium/g11n/resources/php/en_US/validation/default.php
index fde2f76..8c398d5 100644
--- a/libraries/lithium/g11n/resources/php/en_US/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/en_US/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/fr/message/default.php b/libraries/lithium/g11n/resources/php/fr/message/default.php
new file mode 100644
index 0000000..9549fa7
--- /dev/null
+++ b/libraries/lithium/g11n/resources/php/fr/message/default.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * Message data for `fr`.
+ *
+ * Plural rule and forms derived from the GNU gettext documentation.
+ *
+ * @link http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms
+ */
+return array(
+ 'pluralForms' => 2,
+ 'pluralRule' => function ($n) { return $n == 1 ? 0 : 1; }
+);
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/resources/php/fr_BE/validation/default.php b/libraries/lithium/g11n/resources/php/fr_BE/validation/default.php
index 31baadc..870ed0f 100644
--- a/libraries/lithium/g11n/resources/php/fr_BE/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/fr_BE/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/fr_CA/validation/default.php b/libraries/lithium/g11n/resources/php/fr_CA/validation/default.php
index 0dbe28e..30f05c2 100644
--- a/libraries/lithium/g11n/resources/php/fr_CA/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/fr_CA/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/it_IT/validation/default.php b/libraries/lithium/g11n/resources/php/it_IT/validation/default.php
index fe6b691..aaf75aa 100644
--- a/libraries/lithium/g11n/resources/php/it_IT/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/it_IT/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/nl_BE/validation/default.php b/libraries/lithium/g11n/resources/php/nl_BE/validation/default.php
index 8058f46..c49d947 100644
--- a/libraries/lithium/g11n/resources/php/nl_BE/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/nl_BE/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/g11n/resources/php/nl_NL/validation/default.php b/libraries/lithium/g11n/resources/php/nl_NL/validation/default.php
index b6bb938..5907ce6 100644
--- a/libraries/lithium/g11n/resources/php/nl_NL/validation/default.php
+++ b/libraries/lithium/g11n/resources/php/nl_NL/validation/default.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/net/Message.php b/libraries/lithium/net/Message.php
new file mode 100644
index 0000000..ec682b4
--- /dev/null
+++ b/libraries/lithium/net/Message.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\net;
+
+use ReflectionClass;
+use ReflectionProperty;
+
+/**
+ * Base message class for any URI based request/response.
+ * @see http://tools.ietf.org/html/rfc3986#section-1.1.1
+ * @see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
+ *
+ */
+class Message extends \lithium\core\Object {
+
+ /**
+ * The uri scheme
+ *
+ * @var string
+ */
+ public $scheme = 'tcp';
+
+ /**
+ * The hostname for this endpoint.
+ *
+ * @var string
+ */
+ public $host = 'localhost';
+
+ /**
+ * The port
+ *
+ * @var string
+ */
+ public $port = null;
+
+ /**
+ * Absolute path of the request.
+ *
+ * @var string
+ */
+ public $path = null;
+
+ /**
+ * The username
+ *
+ * @var string
+ */
+ public $username = null;
+
+ /**
+ * Absolute path of the request.
+ *
+ * @var string
+ */
+ public $password = null;
+
+ /**
+ * The body of the message.
+ *
+ * @var array
+ */
+ public $body = array();
+
+ /**
+ * Adds config values to the public properties when a new object is created.
+ *
+ * @param array $config
+ */
+ public function __construct(array $config = array()) {
+ $defaults = array(
+ 'scheme' => 'tcp',
+ 'host' => 'localhost',
+ 'port' => null,
+ 'path' => null,
+ 'username' => null,
+ 'password' => null,
+ 'body' => null,
+ 'message' => null,
+ );
+ $config += $defaults;
+
+ foreach (array_filter($config) as $key => $value) {
+ $this->{$key} = $value;
+ }
+ parent::__construct($config);
+ }
+
+ /**
+ * Add body parts.
+ *
+ * @param mixed $data
+ * @param array $options
+ * - `'buffer'`: split the body string
+ * @return array
+ */
+ public function body($data = null, $options = array()) {
+ $default = array('buffer' => null);
+ $options += $default;
+ $this->body = array_merge((array) $this->body, (array) $data);
+ $body = trim(join("\r\n", $this->body));
+ return ($options['buffer']) ? str_split($body, $options['buffer']) : $body;
+ }
+
+ /**
+ * Converts the data in the record set to a different format, i.e. an array. Available
+ * options: array, url, context, or string.
+ *
+ * @param string $format Format to convert to.
+ * @param array $options
+ * @return mixed
+ */
+ public function to($format, array $options = array()) {
+ switch ($format) {
+ case 'array':
+ $array = array();
+ $class = new ReflectionClass(get_class($this));
+
+ foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
+ $array[$prop->getName()] = $prop->getValue($this);
+ }
+ return $array;
+ case 'url':
+ $host = $this->host . ($this->port ? ":{$this->port}" : '');
+ return "{$this->scheme}://{$host}{$this->path}";
+ case 'context':
+ $defaults = array('content' => $this->body(), 'ignore_errors' => true);
+ return array($this->scheme => $options + $defaults);
+ case 'string':
+ default:
+ return (string) $this;
+ }
+ }
+
+ /**
+ * Magic method to convert object to string.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return (string) $this->body();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/net/Socket.php b/libraries/lithium/net/Socket.php
index 68cba90..d01ea27 100644
--- a/libraries/lithium/net/Socket.php
+++ b/libraries/lithium/net/Socket.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -23,6 +23,22 @@ abstract class Socket extends \lithium\core\Object {
protected $_resource = null;
/**
+ * the classes for the socket
+ *
+ * @var array
+ */
+ protected $_classes = array(
+ 'request' => 'lithium\net\Message',
+ 'response' => 'lithium\net\Message'
+ );
+
+ /**
+ * Auto config
+ *
+ */
+ protected $_autoConfig = array('classes' => 'merge');
+
+ /**
* Constructor.
*
* @param array $config Available configuration options are:
@@ -38,14 +54,12 @@ abstract class Socket extends \lithium\core\Object {
public function __construct(array $config = array()) {
$defaults = array(
'persistent' => false,
- 'protocol' => 'tcp',
+ 'scheme' => 'tcp',
'host' => 'localhost',
- 'login' => 'root',
- 'password' => '',
'port' => 80,
'timeout' => 30
);
- parent::__construct((array) $config + $defaults);
+ parent::__construct($config + $defaults);
}
/**
@@ -72,7 +86,7 @@ abstract class Socket extends \lithium\core\Object {
/**
* Reads from the socket.
*
- * @return mixed The read contents, or `false` if reading failed.
+ * @return object `lithium\net\Message`
*/
abstract public function read();
@@ -85,18 +99,6 @@ abstract class Socket extends \lithium\core\Object {
abstract public function write($data);
/**
- * Aggregates read and write methods into a coherent request response
- *
- * @param mixed $request array or object like `\lithium\net\http\Request`
- * @params array $options
- * - path: path for the current request
- * - classes: array of classes to use
- * - response: a class to use for the response
- * @return boolean response string or object like `\lithium\net\http\Response`
- */
- abstract public function send($message, array $options = array());
-
- /**
* Sets the timeout on the socket *connection*.
*
* @param integer $time Seconds after the connection times out.
@@ -113,6 +115,36 @@ abstract class Socket extends \lithium\core\Object {
abstract public function encoding($charset);
/**
+ * Sets the options to be used in subsequent requests.
+ *
+ * @param array $flags If $values is an array, $flags will be used as the
+ * keys to an associative array of curl options. If $values is not set,
+ * then $flags will be used as the associative array.
+ * @param array $value If set, this array becomes the values for the
+ * associative array of curl options.
+ * @return void
+ */
+ public function set($flags, $value = null) {}
+
+ /**
+ * Aggregates read and write methods into a coherent request response
+ *
+ * @param mixed $message a request object based on `\lithium\net\Message`
+ * @param array $options
+ * - '`response`': a fully-namespaced string for the response object
+ * @return object a response object based on `\lithium\net\Message`
+ */
+ public function send($message = null, array $options = array()) {
+ $defaults = array('response' => $this->_classes['response']);
+ $options += $defaults;
+
+ if ($this->write($message)) {
+ $config = array('message' => $this->read()) + $this->_config;
+ return $this->_instance($options['response'], $config);
+ }
+ }
+
+ /**
* Destructor.
*
* @return void
diff --git a/libraries/lithium/net/http/Base.php b/libraries/lithium/net/http/Base.php
deleted file mode 100644
index 69a5e94..0000000
--- a/libraries/lithium/net/http/Base.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\net\http;
-
-/**
- * Base class for `lithium\net\http\Request` and `lithium\net\http\Response`. Implements basic
- * protocol handling for HTTP-based transactions.
- */
-class Base extends \lithium\core\Object {
-
- /**
- * The full protocol: HTTP/1.1
- *
- * @var string
- */
- public $protocol = 'HTTP/1.1';
-
- /**
- * Specification version number
- *
- * @var string
- */
- public $version = '1.1';
-
- /**
- * headers
- *
- * @var array
- */
- public $headers = array();
-
- /**
- * body
- *
- * @var array
- */
- public $body = array();
-
- /**
- * Add a header to rendered output, or return a single header or full header list.
- *
- * @param string $key
- * @param string $value
- * @return array
- */
- public function headers($key = null, $value = null) {
- if (is_string($key) && strpos($key, ':') === false) {
- if ($value === null) {
- return isset($this->headers[$key]) ? $this->headers[$key] : null;
- }
- if ($value === false) {
- unset($this->headers[$key]);
- return $this->headers;
- }
- }
-
- if (!empty($value)) {
- $this->headers = array_merge($this->headers, array($key => $value));
- } else {
- foreach ((array) $key as $header => $value) {
- if (!is_string($header)) {
- if (preg_match('/(.*?):(.+)/i', $value, $match)) {
- $this->headers[$match[1]] = trim($match[2]);
- }
- } else {
- $this->headers[$header] = $value;
- }
- }
- }
- $headers = array();
-
- foreach ($this->headers as $key => $value) {
- $headers[] = "{$key}: {$value}";
- }
- return $headers;
- }
-
- /**
- * Add body parts.
- *
- * @param mixed $data
- * @return array
- */
- public function body($data = null) {
- $this->body = array_merge((array) $this->body, (array) $data);
- return trim(join("\r\n", $this->body));
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/net/http/Media.php b/libraries/lithium/net/http/Media.php
index 615d8d0..79ead37 100644
--- a/libraries/lithium/net/http/Media.php
+++ b/libraries/lithium/net/http/Media.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\net\http;
-use \Exception;
-use \lithium\util\String;
-use \lithium\core\Libraries;
+use lithium\util\String;
+use lithium\core\Libraries;
+use lithium\net\http\MediaException;
/**
* The `Media` class facilitates content-type mapping (mapping between content-types and file
@@ -25,65 +25,26 @@ use \lithium\core\Libraries;
* you may do the following:
*
* {{{
- * $this->render(array('csv', 'data' => Post::find('all')));
+ * $this->render(array('csv' => Post::find('all')));
* }}}
*/
class Media extends \lithium\core\StaticObject {
/**
* Maps file extensions to content-types. Used to set response types and determine request
- * types. Can be modified with Media::type().
+ * types. Can be modified with `Media::type()`.
*
* @var array
* @see lithium\net\http\Media::type()
*/
- protected static $_types = array(
- 'atom' => 'application/atom+xml',
- 'css' => 'text/css',
- 'form' => 'application/x-www-form-urlencoded',
- 'htm' => array('alias' => 'html'),
- 'html' => array('text/html', 'application/xhtml+xml', '*/*'),
- 'js' => array('application/javascript', 'text/javascript'),
- 'json' => 'application/json',
- 'rss' => 'application/rss+xml',
- 'text' => 'text/plain',
- 'txt' => array('alias' => 'text'),
- 'xml' => array('application/xml', 'text/xml'),
- );
+ protected static $_types = array();
/**
* A map of media handler objects or callbacks, mapped to media types.
*
* @var array
*/
- protected static $_handlers = array(
- 'default' => array(
- 'view' => '\lithium\template\View',
- 'paths' => array(
- 'template' => '{:library}/views/{:controller}/{:template}.{:type}.php',
- 'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
- ),
- 'encode' => false,
- 'decode' => false
- ),
- 'html' => array(),
- 'json' => array(
- 'view' => false,
- 'layout' => false,
- 'encode' => 'json_encode',
- 'decode' => 'json_decode'
- ),
- 'text' => array(
- 'view' => false,
- 'layout' => false,
- 'template' => false
- ),
- 'form' => array(
- 'view' => false,
- 'layout' => false,
- 'encode' => 'http_build_query'
- )
- );
+ protected static $_handlers = array();
/**
* Contains default path settings for various asset types.
@@ -95,24 +56,15 @@ class Media extends \lithium\core\StaticObject {
* @var array
* @see lithium\net\http\Media::assets()
*/
- protected static $_assets = array(
- 'js' => array('suffix' => '.js', 'filter' => null, 'path' => array(
- '{:base}/{:library}/js/{:path}' => array('base', 'library', 'path'),
- '{:base}/js/{:path}' => array('base', 'path')
- )),
- 'css' => array('suffix' => '.css', 'filter' => null, 'path' => array(
- '{:base}/{:library}/css/{:path}' => array('base', 'library', 'path'),
- '{:base}/css/{:path}' => array('base', 'path')
- )),
- 'image' => array('suffix' => null, 'filter' => null, 'path' => array(
- '{:base}/{:library}/img/{:path}' => array('base', 'library', 'path'),
- '{:base}/img/{:path}' => array('base', 'path')
- )),
- 'generic' => array('suffix' => null, 'filter' => null, 'path' => array(
- '{:base}/{:library}/{:path}' => array('base', 'library', 'path'),
- '{:base}/{:path}' => array('base', 'path')
- ))
- );
+ protected static $_assets = array();
+
+ /**
+ * Placeholder for class dependencies. This class' dependencies (i.e. templating classes) are
+ * typically specified through other configuration.
+ *
+ * @var array
+ */
+ protected static $_classes = array();
/**
* Returns the list of registered media types. New types can be set with the `type()` method.
@@ -121,7 +73,7 @@ class Media extends \lithium\core\StaticObject {
* list of types handled.
*/
public static function types() {
- return array_keys(static::$_types);
+ return array_keys(static::_types());
}
/**
@@ -150,58 +102,114 @@ class Media extends \lithium\core\StaticObject {
* @return mixed
*/
public static function to($format, $data, array $options = array()) {
- $data = is_object($data) ? $data->to('array') : $data;
return static::encode($format, $data, $options);
}
/**
- * Map an extension to a particular content-type (or types) with a set of 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:
- * {{{// Get a list of all available media types:
- * Media::types(); // returns array('ai', 'amf', 'atom', ...);
- * }}}
+ * {{{ embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(1-2) }}}
*
- * {{{// Add a custom media type:
- * Media::type('my', 'text/x-my', array('view' => '\my\custom\View', 'layout' => false));
- * }}}
+ * {{{ embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(19-20) }}}
*
- * {{{// Remove a custom media type:
- * Media::type('my', false);
- * }}}
+ * {{{ embed:lithium\tests\cases\net\http\MediaTest::testMediaTypes(35-36) }}}
*
* 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', array('application/xhtml+xml', 'text/html'));
* }}}
*
- * @param string $type A file extension for the type, i.e. `'txt'`, `'js'`, or `'atom'`.
- * Alternatively, may be a content type, i.e. `'text/html'`,
- * `'application/atom+xml'`, etc.; in which case, the type name (i.e. '`html'` or
- * `'atom'`) will be returned.
+ * 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](http://en.wikipedia.org/wiki/JSON#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', array('text/html'), array(
+ * // template settings...
+ * 'conditions' => array('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
+ * varialbes (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.
+ *
+ * @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\String::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.
+ * 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:
- * - `'decode'`: A (string) function name or (object) closure that handles
+ * - `'decode'` _mixed_: A (string) function name or (object) closure that handles
* decoding or unserializing content from this format.
- * - `'encode'`: A (string) function name or (object) closure that handles encoding or
- * serializing content into this format.
- * - `'layout'`: Specifies a `String::insert()`-style path to use when searching for
- * layout files.
- * - `'template'`: Specifies a `String::insert()`-style path to use when searching for
- * template files.
- * - `'view'`: Specifies the view class to use when rendering this content.
+ * - `'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.
+ * - `'layout'` _mixed_: Specifies one or more `String::insert()`-style paths to use when
+ * searching for layout files (either a string or array of strings).
+ * - `'template'` _mixed_: Specifies one or more `String::insert()`-style paths to use
+ * when searching for template files (either a string or array of strings).
+ * - `'view'` _string_: Specifies the view class to use when rendering this content.
+ * - `'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`.
- * @see lithium\net\http\Media::$_types
- * @see lithium\net\http\Media::$_handlers
- * @see lithium\util\String::insert()
*/
public static function type($type, $content = null, array $options = array()) {
$defaults = array(
@@ -209,37 +217,125 @@ class Media extends \lithium\core\StaticObject {
'template' => false,
'layout' => false,
'encode' => false,
- 'decode' => false
+ 'decode' => false,
+ 'cast' => true,
+ 'conditions' => array(),
);
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') + array('options' => static::_handlers($type));
+ }
+ if ($content) {
+ static::$_types[$type] = $content;
+ }
+ static::$_handlers[$type] = $options ? ($options + $defaults) : array();
+ }
+
+ /**
+ * 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 object $request The instance of `lithium\action\Request` which contains the details of
+ * the request to be content-negotiated.
+ * @return string Returns the first matching type name, i.e. `'html'` or `'json'`.
+ */
+ public static function negotiate($request) {
+ $self = get_called_class();
+
+ $match = function($name) use ($self, $request) {
+ if (($cfg = $self::type($name)) && $self::match($request, compact('name') + $cfg)) {
+ return true;
+ }
+ return false;
+ };
+
+ if (($type = $request->type) && $match($type)) {
+ return $type;
+ }
- if (strpos($type, '/')) {
- foreach (static::$_types as $name => $cTypes) {
- if ($type == $cTypes || (is_array($cTypes) && in_array($type, $cTypes))) {
- return $name;
+ foreach ($request->accepts(true) as $type) {
+ if (!$types = (array) static::_types($type)) {
+ continue;
+ }
+ foreach ($types as $name) {
+ if (!$match($name)) {
+ continue;
}
+ return $name;
}
- return;
}
+ }
- if (empty($content) && empty($options)) {
- $content = isset(static::$_types[$type]) ? static::$_types[$type] : null;
- $options = isset(static::$_handlers[$type]) ? static::$_handlers[$type] : null;
- return compact('content', 'options');
+ /**
+ * 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 object $request The instance of `lithium\action\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'];
- if (!empty($content)) {
- static::$_types[$type] = $content;
+ 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;
+ }
}
- static::$_handlers[$type] = !empty($options) ? ((array) $options + $defaults) : array();
+ return true;
}
/**
* Gets or sets options for various asset types.
*
+ * @see lithium\util\String::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:
@@ -254,42 +350,43 @@ class Media extends \lithium\core\StaticObject {
* 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`.
- * @see lithium\util\String::insert()
*/
public static function assets($type = null, $options = array()) {
$defaults = array('suffix' => null, 'filter' => null, 'path' => array());
- if (empty($type)) {
- return static::$_assets;
+ if (!$type) {
+ return static::_assets();
}
if ($options === false) {
unset(static::$_assets[$type]);
}
- if (empty($options)) {
- return isset(static::$_assets[$type]) ? static::$_assets[$type] : null;
+ if (!$options) {
+ return static::_assets($type);
}
- $options += $defaults;
+ $options = (array) $options + $defaults;
- if (isset(static::$_assets[$type])) {
- static::$_assets[$type] = array_filter((array) $options) + static::$_assets[$type];
- } else {
- static::$_assets[$type] = $options;
+ 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`.
+ * @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`.
+ * - `'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.
* - `'path'`: An array of paths to search for the asset in. The paths should use
@@ -297,12 +394,12 @@ class Media extends \lithium\core\StaticObject {
* - `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.
- * @see lithium\net\http\Media::$_assets
- * @see lithium\action\Request::env()
* @filter
*/
public static function asset($path, $type, array $options = array()) {
@@ -313,44 +410,44 @@ class Media extends \lithium\core\StaticObject {
'path' => array(),
'suffix' => null,
'check' => false,
- 'library' => 'app'
+ 'library' => true,
);
- $type = isset(static::$_assets[$type]) ? $type : 'generic';
- $options += (static::$_assets[$type] + $defaults);
+ if (!$base = static::_assets($type)) {
+ $type = 'generic';
+ $base = static::_assets('generic');
+ }
+ $options += ($base + $defaults);
$params = compact('path', 'type', 'options');
- return static::_filter(__FUNCTION__, $params, function($self, $params, $chain) {
- extract($params);
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $path = $params['path'];
+ $type = $params['type'];
+ $options = $params['options'];
+ $library = $options['library'];
if (preg_match('/^[a-z0-9-]+:\/\//i', $path)) {
return $path;
}
-
- $library = isset($options['plugin']) ? $options['plugin'] : $options['library'];
$config = Libraries::get($library);
$paths = $options['path'];
- ($library == 'app') ? end($paths) : reset($paths);
+ $config['default'] ? end($paths) : reset($paths);
$options['library'] = basename($config['path']);
- unset($options['plugin']);
if ($options['suffix'] && strpos($path, $options['suffix']) === false) {
$path .= $options['suffix'];
}
- $file = $config['path'] . '/webroot';
- if ($path[0] == '/') {
- $result = "{$options['base']}{$path}";
- $file .= $path;
- } else {
- $result = String::insert(key($paths), compact('path') + $options);
- $realPath = str_replace('{:library}/', '', key($paths));
- $file = String::insert($realPath, array('base' => $file) + compact('path'));
+ if ($options['check'] || $options['timestamp']) {
+ $file = $self::path($path, $type, $options);
}
- $path = $result;
- if ($qOffset = strpos($file, '?')) {
- $file = substr($file, 0, $qOffset);
+ if ($path[0] === '/') {
+ if ($options['base'] && strpos($path, $options['base']) !== 0) {
+ $path = "{$options['base']}{$path}";
+ }
+ } else {
+ $path = String::insert(key($paths), compact('path') + $options);
}
if ($options['check'] && !is_file($file)) {
@@ -372,6 +469,72 @@ class Media extends \lithium\core\StaticObject {
}
/**
+ * 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.
+ * @return string Returns the physical path to the web assets directory for a library. For
+ * example, the `/webroot` directory of the default library would be
+ * `LITHIUM_APP_PATH . '/webroot'`.
+ */
+ public static function webroot($library = true) {
+ 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 $path 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 = array()) {
+ $defaults = array(
+ 'base' => null,
+ 'path' => array(),
+ 'suffix' => null,
+ 'library' => true,
+ );
+ if (!$base = static::_assets($type)) {
+ $type = 'generic';
+ $base = static::_assets('generic');
+ }
+ $options += ($base + $defaults);
+ $config = Libraries::get($options['library']);
+ $root = static::webroot($options['library']);
+ $paths = $options['path'];
+
+ $config['default'] ? end($paths) : reset($paths);
+ $options['library'] = basename($config['path']);
+
+ if ($qOffset = strpos($path, '?')) {
+ $path = substr($path, 0, $qOffset);
+ }
+
+ if ($path[0] === '/') {
+ $file = $root . $path;
+ } else {
+ $template = str_replace('{:library}/', '', key($paths));
+ $insert = array('base' => $root) + compact('path');
+ $file = String::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.
*
@@ -382,34 +545,70 @@ class Media extends \lithium\core\StaticObject {
* @param array $options
* @return void
* @filter
- * @todo Implement proper exception handling
*/
public static function render(&$response, $data = null, array $options = array()) {
$params = array('response' => &$response) + compact('data', 'options');
- $types = static::$_types;
- $handlers = static::$_handlers;
+ $types = static::_types();
+ $handlers = static::_handlers();
static::_filter(__FUNCTION__, $params, function($self, $params) use ($types, $handlers) {
- $defaults = array(
- 'encode' => null, 'template' => null, 'layout' => null, 'view' => null
- );
+ $defaults = array('encode' => null, 'template' => null, 'layout' => '', 'view' => null);
$response =& $params['response'];
$data = $params['data'];
$options = $params['options'] + array('type' => $response->type());
$result = null;
$type = $options['type'];
- $hasHandler = isset($handlers[$type]);
- $handler = $options + ($hasHandler ? $handlers[$type] : array()) + $defaults;
- if ((!$handler['encode'] && !$handler['view']) && !$hasHandler) {
- throw new Exception("Unhandled media type '{$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;
- $response->body($self::invokeMethod('_handle', array($handler, $data, $options)));
- $response->headers('Content-type', current((array) $types[$type]));
+ if (isset($types[$type])) {
+ $header = current((array) $types[$type]);
+ $header .= $response->encoding ? "; charset={$response->encoding}" : '';
+ $response->headers('Content-type', $header);
+ }
+ $response->body($self::invokeMethod('_handle', array($handler, $data, $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`.
+ */
+ public static function view($handler, $data, &$response = null, array $options = array()) {
+ $params = array('response' => &$response) + compact('handler', 'data', 'options');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $data = $params['data'];
+ $options = $params['options'];
+ $handler = $params['handler'];
+ $response =& $params['response'];
+
+ if (!is_array($handler)) {
+ $handler = $self::invokeMethod('_handlers', array($handler));
+ }
+ $class = $handler['view'];
+ unset($handler['view']);
+
+ $config = $handler + array('response' => &$response);
+ return $self::invokeMethod('_instance', array($class, $config));
});
}
@@ -417,19 +616,50 @@ class Media extends \lithium\core\StaticObject {
* For media types registered in `$_handlers` which include an `'encode'` setting, encodes 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`.
+ * @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.
* @param array $options Handler-specific options.
- * @return mixed
+ * @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($type, $data, array $options = array()) {
- if (!isset(static::$_handlers[$type])) {
- return null;
- }
- $method = static::$_handlers[$type]['encode'];
- return is_string($method) ? $method($data) : $method($data, $handler + $options);
+ public static function encode($handler, $data, &$response = null) {
+ $params = array('response' => &$response) + compact('handler', 'data');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $data = $params['data'];
+ $handler = $params['handler'];
+ $response =& $params['response'];
+
+ if (!is_array($handler)) {
+ $handler = $self::invokeMethod('_handlers', array($handler));
+ }
+
+ if (!$handler || !isset($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);
+ });
}
/**
@@ -444,55 +674,174 @@ class Media extends \lithium\core\StaticObject {
* @return mixed
*/
public static function decode($type, $data, array $options = array()) {
- if (!isset(static::$_handlers[$type])) {
+ if ((!$handler = static::_handlers($type)) || !isset($handler['decode'])) {
return null;
}
- $method = static::$_handlers[$type]['decode'];
+ $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.
+ *
+ * @return void
+ */
+ public static function reset() {
+ foreach (get_class_vars(__CLASS__) as $name => $value) {
+ static::${$name} = array();
+ }
+ }
+
+ /**
* 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 array $options
+ * @param object $response A reference to the `Response` object for this dispatch cycle.
* @return string
* @filter
*/
- protected static function _handle($handler, $data, $options) {
- $params = compact('handler', 'data', 'options');
- return static::_filter(__FUNCTION__, $params, function($self, $params, $chain) {
- extract($params, EXTR_OVERWRITE);
- $result = '';
+ protected static function _handle($handler, $data, &$response) {
+ $params = array('response' => &$response) + compact('handler', 'data');
+
+ return static::_filter(__FUNCTION__, $params, function($self, $params) {
+ $response = $params['response'];
+ $handler = $params['handler'];
+ $data = $params['data'];
+ $options = $handler;
if (isset($options['request'])) {
- $options += (array) $options['request']->params;
- $handler['request'] = $options['request'];
+ $options += $options['request']->params;
+ unset($options['request']);
}
switch (true) {
case $handler['encode']:
- $method = $handler['encode'];
- $result = is_string($method) ? $method($data) : $method(
- $data, $handler, $options
- );
- break;
- case $handler['view']:
- $view = new $handler['view']($handler);
- $result = $view->render('all', $data, $options);
- break;
+ return $self::encode($handler, $data, $response);
case ($handler['template'] === false) && is_string($data):
- $result = $data;
- break;
+ return $data;
+ case $handler['view']:
+ unset($options['view']);
+ $instance = $self::view($handler, $data, $response, $options);
+ return $instance->render('all', (array) $data, $options);
default:
- $result = print_r($data, true);
- break;
+ throw new MediaException("Could not interpret type settings for handler.");
}
- return $result;
});
}
+
+ /**
+ * Helper method for listing registered media types. Returns all types, or a single
+ * content type if a specific type is specified.
+ *
+ * @param string $type Type to return.
+ * @return mixed Array of types, or single type requested.
+ */
+ protected static function _types($type = null) {
+ $types = static::$_types + array(
+ 'html' => array('text/html', 'application/xhtml+xml', '*/*'),
+ 'htm' => array('alias' => 'html'),
+ 'form' => 'application/x-www-form-urlencoded',
+ 'json' => 'application/json',
+ 'rss' => 'application/rss+xml',
+ 'atom' => 'application/atom+xml',
+ 'css' => 'text/css',
+ 'js' => array('application/javascript', 'text/javascript'),
+ 'text' => 'text/plain',
+ 'txt' => array('alias' => 'text'),
+ 'xml' => array('application/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);
+ }
+ $result = array();
+
+ 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.
+ */
+ protected static function _handlers($type = null) {
+ $handlers = static::$_handlers + array(
+ 'default' => array(
+ 'view' => 'lithium\template\View',
+ 'encode' => false,
+ 'decode' => false,
+ 'cast' => false,
+ 'paths' => array(
+ 'template' => '{:library}/views/{:controller}/{:template}.{:type}.php',
+ 'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
+ 'element' => '{:library}/views/elements/{:template}.{:type}.php',
+ )
+ ),
+ 'html' => array(),
+ 'json' => array('cast' => true, 'encode' => 'json_encode', 'decode' => function($data) {
+ return json_decode($data, true);
+ }),
+ 'text' => array('cast' => false, 'encode' => function($s) { return $s; }),
+ 'form' => array('cast' => true, 'encode' => 'http_build_query'),
+ );
+
+ 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 + array(
+ 'js' => array('suffix' => '.js', 'filter' => null, 'path' => array(
+ '{:base}/{:library}/js/{:path}' => array('base', 'library', 'path'),
+ '{:base}/js/{:path}' => array('base', 'path')
+ )),
+ 'css' => array('suffix' => '.css', 'filter' => null, 'path' => array(
+ '{:base}/{:library}/css/{:path}' => array('base', 'library', 'path'),
+ '{:base}/css/{:path}' => array('base', 'path')
+ )),
+ 'image' => array('suffix' => null, 'filter' => null, 'path' => array(
+ '{:base}/{:library}/img/{:path}' => array('base', 'library', 'path'),
+ '{:base}/img/{:path}' => array('base', 'path')
+ )),
+ 'generic' => array('suffix' => null, 'filter' => null, 'path' => array(
+ '{:base}/{:library}/{:path}' => array('base', 'library', 'path'),
+ '{:base}/{:path}' => array('base', 'path')
+ ))
+ );
+ if ($type) {
+ return isset($assets[$type]) ? $assets[$type] : null;
+ }
+ return $assets;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/net/http/MediaException.php b/libraries/lithium/net/http/MediaException.php
new file mode 100644
index 0000000..d99d61c
--- /dev/null
+++ b/libraries/lithium/net/http/MediaException.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\net\http;
+
+/**
+ * The `MediaException` is thrown when a request is made to render content in a format not
+ * supported.
+ *
+ * @see lithium\net\http\Media
+ */
+class MediaException extends \RuntimeException {
+
+ protected $code = 415;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/net/http/Message.php b/libraries/lithium/net/http/Message.php
new file mode 100644
index 0000000..98c866d
--- /dev/null
+++ b/libraries/lithium/net/http/Message.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\net\http;
+
+/**
+ * Base class for `lithium\net\http\Request` and `lithium\net\http\Response`. Implements basic
+ * protocol handling for HTTP-based transactions.
+ */
+class Message extends \lithium\net\Message {
+
+ /**
+ * The full protocol: HTTP/1.1
+ *
+ * @var string
+ */
+ public $protocol = 'HTTP/1.1';
+
+ /**
+ * Specification version number
+ *
+ * @var string
+ */
+ public $version = '1.1';
+
+ /**
+ * headers
+ *
+ * @var array
+ */
+ public $headers = array();
+
+ /**
+ * body
+ *
+ * @var array
+ */
+ public $body = array();
+
+ /**
+ * Content-Type
+ *
+ * @var string
+ */
+ protected $_type = 'html';
+
+ /**
+ * Classes used by `Request`.
+ *
+ * @var array
+ */
+ protected $_classes = array('media' => 'lithium\net\http\Media');
+
+ /**
+ * Add a header to rendered output, or return a single header or full header list.
+ *
+ * @param string $key
+ * @param string $value
+ * @return array
+ */
+ public function headers($key = null, $value = null) {
+ if (is_string($key) && strpos($key, ':') === false) {
+ if ($value === null) {
+ return isset($this->headers[$key]) ? $this->headers[$key] : null;
+ }
+ if ($value === false) {
+ unset($this->headers[$key]);
+ return $this->headers;
+ }
+ }
+
+ if ($value) {
+ $this->headers = array_merge($this->headers, array($key => $value));
+ } else {
+ foreach ((array) $key as $header => $value) {
+ if (!is_string($header)) {
+ if (preg_match('/(.*?):(.+)/i', $value, $match)) {
+ $this->headers[$match[1]] = trim($match[2]);
+ }
+ } else {
+ $this->headers[$header] = $value;
+ }
+ }
+ }
+ $headers = array();
+
+ foreach ($this->headers as $key => $value) {
+ $headers[] = "{$key}: {$value}";
+ }
+ return $headers;
+ }
+
+ /**
+ * Sets/Gets the content type
+ *
+ * @param string $type a full content type i.e. `'application/json'` or simple name `'json'`
+ * @return string A simple content type name, i.e. `'html'`, `'xml'`, `'json'`, etc., depending
+ * on the content type of the request.
+ */
+ public function type($type = null) {
+ if ($type === null) {
+ return $this->_type;
+ }
+ if (strpos($type, '/')) {
+ $media = $this->_classes['media'];
+
+ if ($data = $media::type($type)) {
+ $type = is_array($data) ? reset($data) : $data;
+ }
+ }
+ return $this->_type = $type;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/net/http/Request.php b/libraries/lithium/net/http/Request.php
index 8e07972..b563be3 100644
--- a/libraries/lithium/net/http/Request.php
+++ b/libraries/lithium/net/http/Request.php
@@ -2,41 +2,19 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\net\http;
-use \lithium\util\String;
+use lithium\util\String;
/**
* Facilitates HTTP request creation by assembling connection and path info, `GET` and `POST` data,
* and authentication credentials in a single, stateful object.
*/
-class Request extends \lithium\net\http\Base {
-
- /**
- * The protocol scheme to be used in the request. Used when calculating the target URL of this
- * request's end point.
- *
- * @var string
- */
- public $scheme = 'http';
-
- /**
- * The Host header value and authority.
- *
- * @var string
- */
- public $host = 'localhost';
-
- /**
- * Port number.
- *
- * @var string
- */
- public $port = 80;
+class Request extends \lithium\net\http\Message {
/**
* The method of the request, typically one of the following: `GET`, `POST`, `PUT`, `DELETE`,
@@ -47,13 +25,6 @@ class Request extends \lithium\net\http\Base {
public $method = 'GET';
/**
- * Absolute path of the request.
- *
- * @var string
- */
- public $path = '/';
-
- /**
* Used to build query string.
*
* @var array
@@ -67,7 +38,7 @@ class Request extends \lithium\net\http\Base {
* {{{
* array(
* 'Host' => $this->host . ":" . $this->port,
- * 'Connection' => 'Close', 'User-Agent' => 'Mozilla/5.0 (Lithium)'
+ * 'Connection' => 'Close', 'User-Agent' => 'Mozilla/5.0'
* )
* }}}
* @var array
@@ -75,17 +46,6 @@ class Request extends \lithium\net\http\Base {
public $headers = array();
/**
- * The authentication/authorization information
- *
- * For example:
- * {{{
- * array('method' => 'Basic', 'username' => 'lithium', 'password' => 'rad')
- * }}}
- * @var array
- */
- public $auth = array();
-
- /**
* Cookies.
*
* @var array
@@ -93,48 +53,37 @@ class Request extends \lithium\net\http\Base {
public $cookies = array();
/**
- * Body.
- *
- * @var array
- */
- public $body = array();
-
- /**
* Constructor
*
* @param array $config
+ * - auth: the Authorization method (Basic|Digest)
+ * - username: the username for auth
+ * - password: the password for auth
* @return object
*/
public function __construct(array $config = array()) {
$defaults = array(
'scheme' => 'http',
'host' => 'localhost',
- 'port' => 80,
+ 'port' => null,
'method' => 'GET',
'path' => '/',
+ 'auth' => null,
'headers' => array(),
'body' => array(),
'params' => array()
);
$config += $defaults;
+ parent::__construct($config);
- foreach ($config as $key => $value) {
- $this->{$key} = $value;
- }
$this->protocol = "HTTP/{$this->version}";
-
$this->headers = array(
- 'Host' => $this->host . ":" . $this->port,
+ 'Host' => $this->port ? "{$this->host}:{$this->port}" : $this->host,
'Connection' => 'Close',
- 'User-Agent' => 'Mozilla/5.0 (Lithium)'
+ 'User-Agent' => 'Mozilla/5.0',
);
$this->headers($config['headers']);
- if (!empty($config['auth']['password'])) {
- $this->headers('Authorization', $config['auth']['method'] . ' ' . base64_encode(
- $config['auth']['username'] . ':' . $config['auth']['password']
- ));
- }
if (strpos($this->host, '/') !== false) {
$parts = explode('/', $this->host, 2);
$this->host = $parts[0];
@@ -150,7 +99,7 @@ class Request extends \lithium\net\http\Base {
* @return array
*/
public function queryString($params = array(), $format = "{:key}={:value}&") {
- if (empty($params)) {
+ if (!$params) {
if (is_string($this->params)) {
return "?" . $this->params;
}
@@ -161,35 +110,80 @@ class Request extends \lithium\net\http\Base {
$query = null;
foreach ($params as $key => $value) {
- $query .= String::insert($format, array(
- 'key' => urlencode($key), 'value' => urlencode($value)
- ));
+ $values = array('key' => urlencode($key), 'value' => urlencode($value));
+ $query .= String::insert($format, $values);
}
- if (empty($query)) {
+ if (!$query) {
return null;
}
return "?" . $this->params = substr($query, 0, -1);
}
+ /**
+ * Converts the data in the record set to a different format, i.e. an array. Available
+ * options: array, URL, stream context configuration, or string.
+ *
+ * @see lithium\net\Message::to()
+ * @param string $format Format to convert to. Should be either `'url'`, which returns a string
+ * representation of the URL that this request points to, or `'context'`, which
+ * returns an array usable with PHP's `stream_context_create()` function. For
+ * more available formats, see the parent method, `lithium\net\Message::to()`.
+ * @param array $options Allows overriding of specific portions of the URL, as follows. These
+ * options should only be specified if you intend to replace the values that are
+ * already in the `Request` object.
+ * - `'scheme'` _string_: The protocol scheme of the URL.
+ * - `'method'` _string_: If applicable, the HTTP method to use in the request.
+ * Mainly applies to the `'context'` format.
+ * - `'host'` _string_: The host name the request is pointing at.
+ * - `'port'` _string_: The host port, if any. If specified, should be prefixed
+ * with `':'`.
+ * - `'path'` _string_: The URL path.
+ * - `'query'` _mixed_: The query string of the URL as a string or array. If passed
+ * as a string, should be prefixed with `'?'`.
+ * - `'auth'` _string_: Authentication information. See the constructor for
+ * details.
+ * - `'content'` _string_: The body of the request.
+ * - `'headers'` _array_: The request headers.
+ * - `'version'` _string_: The HTTP version of the request, where applicable.
+ * @return mixed Varies; see the `$format` parameter for possible return values.
+ */
public function to($format, array $options = array()) {
+ $defaults = array(
+ 'method' => $this->method,
+ 'scheme' => $this->scheme,
+ 'host' => $this->host,
+ 'port' => $this->port ? ":{$this->port}" : '',
+ 'path' => $this->path,
+ 'query' => $this->queryString(),
+ 'auth' => $this->_config['auth'],
+ 'content' => $this->body(),
+ 'version' => $this->version,
+ );
+ $options += $defaults;
+
+ if ($options['query'] && is_array($options['query'])) {
+ $options['query'] = $this->queryString($options['query']);
+ }
+
switch ($format) {
- case 'array':
- return array(
- 'method' => $this->method,
- 'content' => $this->body(),
- 'header' => $this->headers()
- );
case 'url':
- $query = $this->queryString();
- return "{$this->scheme}://{$this->host}:{$this->port}{$this->path}{$query}";
+ return String::insert("{:scheme}://{:host}{:port}{:path}{:query}", $options);
case 'context':
- return array($this->scheme => $options + $this->to('array') + array(
- 'method' => null, 'content' => null,
- 'ignore_errors' => true, 'timeout' => 1
- ));
- case 'string':
+ if ($options['auth']) {
+ $auth = base64_encode("{$this->username}:{$this->password}");
+ $this->headers('Authorization', "{$options['auth']} {$auth}");
+ }
+ $this->headers('Content-Length', strlen($options['content']));
+ $base = array(
+ 'content' => $options['content'],
+ 'method' => $options['method'],
+ 'header' => $this->headers(),
+ 'protocol_version' => $options['version'],
+ 'ignore_errors' => true
+ );
+ return array('http' => array_diff_key($options, $defaults) + $base);
default:
- return (string) $this;
+ return parent::to($format, $options);
}
}
@@ -199,18 +193,17 @@ class Request extends \lithium\net\http\Base {
* @return string
*/
public function __toString() {
- $query = $this->queryString();
- $path = str_replace('//', '/', $this->path) . $query;
-
+ if (!empty($this->_config['auth'])) {
+ $this->headers('Authorization', "{$this->_config['auth']} " . base64_encode(
+ "{$this->username}:{$this->password}"
+ ));
+ }
+ $path = str_replace('//', '/', $this->path) . $this->queryString();
$body = $this->body();
$this->headers('Content-Length', strlen($body));
- $request = array(
- "{$this->method} {$path} {$this->protocol}",
- join("\r\n", $this->headers()),
- "", $body
- );
- return join("\r\n", $request);
+ $status = "{$this->method} {$path} {$this->protocol}";
+ return join("\r\n", array($status, join("\r\n", $this->headers()), "", $body));
}
}
diff --git a/libraries/lithium/net/http/Response.php b/libraries/lithium/net/http/Response.php
index 98063d6..652e434 100644
--- a/libraries/lithium/net/http/Response.php
+++ b/libraries/lithium/net/http/Response.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -11,7 +11,7 @@ namespace lithium\net\http;
/**
* Parses and stores the status, headers and body of an HTTP response.
*/
-class Response extends \lithium\net\http\Base {
+class Response extends \lithium\net\http\Message {
/**
* Status code and message.
@@ -35,11 +35,11 @@ class Response extends \lithium\net\http\Base {
public $type = 'text/html';
/**
- * Character Set.
+ * Character encoding.
*
* @var string
*/
- public $charset = 'UTF-8';
+ public $encoding = 'UTF-8';
/**
* The body.
@@ -95,65 +95,55 @@ class Response extends \lithium\net\http\Base {
504 => 'Gateway Time-out'
);
- /**
- * Constructor.
- *
- * @param array $config
- * @return object
- */
- public function __construct(array $config = array()) {
- $defaults = array('message' => '');
- parent::__construct((array) $config + $defaults);
- }
-
protected function _init() {
- if (!empty($this->_config['message'])) {
- $parts = explode("\r\n\r\n", $this->_config['message']);
+ parent::_init();
+ $body = $this->_config['body'];
- if (empty($parts)) {
- return;
- }
- $headers = str_replace("\r", "", explode("\n", array_shift($parts)));
+ if ($this->_config['body'] && !$this->_config['message']) {
+ $this->body = $this->_config['body'];
+ } elseif (($body = $this->_config['message']) && !$this->_config['body']) {
+ $body = $this->_parseMessage($body);
+ }
- if (array_filter($headers) == array()) {
- return;
- }
- preg_match('/HTTP\/(\d+\.\d+)\s+(\d+)\s+(.*)/i', array_shift($headers), $match);
+ if (isset($this->headers['Content-Type'])) {
+ preg_match('/^(.*?);\s*?charset=(.+)/i', $this->headers['Content-Type'], $match);
- if (!empty($match)) {
- list($line, $this->version, $code, $message) = $match;
- $this->status = compact('code', 'message') + $this->status;
+ if ($match) {
+ $this->type = trim($match[1]);
+ $this->encoding = strtoupper(trim($match[2]));
}
- $this->protocol = "HTTP/{$this->version}";
- $this->headers($headers);
+ }
- if (!empty($this->headers['Content-Type'])) {
- preg_match('/^(.*?);charset=(.+)/i', $this->headers['Content-Type'], $match);
+ if (isset($this->headers['Transfer-Encoding'])) {
+ $body = $this->_decode($body);
+ }
+ $this->body = $this->body ?: $body;
+ }
- if (!empty($match)) {
- $this->type = trim($match[1]);
- $this->charset = trim($match[2]);
- }
- }
- $body = implode("\r\n\r\n", $parts);
+ protected function _parseMessage($body) {
+ if (!($parts = explode("\r\n\r\n", $body, 2)) || count($parts) == 1) {
+ return $body;
+ }
+ list($headers, $body) = $parts;
+ $headers = str_replace("\r", "", explode("\n", $headers));
- if (isset($this->headers['Transfer-Encoding'])) {
- $body = $this->_decode($body);
- }
- $this->body($body);
- unset($this->_config['message']);
+ if (array_filter($headers) == array()) {
+ return $body;
}
+ preg_match('/HTTP\/(\d+\.\d+)\s+(\d+)\s+(.*)/i', array_shift($headers), $match);
+ $this->headers($headers);
- foreach ((array) $this->_config as $key => $value) {
- if (isset($this->{$key})) {
- $this->{$key} = $value;
- unset($this->_config[$key]);
- }
+ if (!$match) {
+ return $body;
}
+ list($line, $this->version, $code, $message) = $match;
+ $this->status = compact('code', 'message') + $this->status;
+ $this->protocol = "HTTP/{$this->version}";
+ return $body;
}
/**
- * undocumented function
+ * Set and get the status for the response
*
* @param string $key
* @param string $data
@@ -163,8 +153,9 @@ class Response extends \lithium\net\http\Base {
if ($data === null) {
$data = $key;
}
- if (!empty($data)) {
- $this->status = array('code'=> null, 'message' => null);
+ if ($data) {
+ $this->status = array('code' => null, 'message' => null);
+
if (is_numeric($data) && isset($this->_statuses[$data])) {
$this->status = array('code' => $data, 'message' => $this->_statuses[$data]);
} else {
@@ -190,6 +181,9 @@ class Response extends \lithium\net\http\Base {
* @return string
*/
public function __toString() {
+ if ($this->type != 'text/html' && !isset($this->headers['Content-Type'])) {
+ $this->headers['Content-Type'] = $this->type;
+ }
$first = "{$this->protocol} {$this->status['code']} {$this->status['message']}";
$response = array($first, join("\r\n", $this->headers()), "", $this->body());
return join("\r\n", $response);
diff --git a/libraries/lithium/net/http/Route.php b/libraries/lithium/net/http/Route.php
index 2a214bf..d3f9d98 100644
--- a/libraries/lithium/net/http/Route.php
+++ b/libraries/lithium/net/http/Route.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -55,8 +55,9 @@ class Route extends \lithium\core\Object {
*
* This string can contain fixed elements, i.e. `"/admin"`, capture elements,
* i.e. `"/{:controller}"`, capture elements optionally paired with regular expressions or
- * named regular expression patterns, i.e. `"/{:id:\d+}"` or `"/{:id:ID}"`, wildcard captures
- * i.e. `"**"`, or any combination thereof, i.e. `"/admin/{:controller}/{:id:ID}/**"`.
+ * named regular expression patterns, i.e. `"/{:id:\d+}"` or `"/{:id:ID}"`, the special wildcard
+ * capture, i.e. `"{:args}"`, or any combination thereof, i.e.
+ * `"/admin/{:controller}/{:id:\d+}/{:args}"`.
*
* @var string
*/
@@ -75,38 +76,116 @@ class Route extends \lithium\core\Object {
*/
protected $_pattern = '';
+ /**
+ * An array of route parameter names (i.e. {:foo}) that appear in the URL template.
+ *
+ * @var array
+ * @see lithium\net\http\Route::$_template
+ */
protected $_keys = array();
+ /**
+ * An array of key/value pairs representing the parameters of the route. For keys which match
+ * parameters present in the route template, the corresponding values match the default values
+ * of those parameters. Specifying a default value for a template parameter makes that
+ * parameter optional. Any other pairs specified must match exactly when doing a reverse lookup
+ * in order for the route to match.
+ *
+ * @var array
+ */
protected $_params = array();
+ /**
+ * The array of values that appear in the second parameter of `Router::connect()`, which are
+ * **not** present in the URL template. When matching a route, these parameters must appear
+ * **exactly** as specified here.
+ *
+ * @var array
+ */
protected $_match = array();
+ /**
+ * An array of metadata parameters which must be present in the request in order for the route
+ * to match.
+ *
+ * @var array
+ */
+ protected $_meta = array();
+
+ /**
+ * The default values for the keys present in the URL template.
+ *
+ * @var array
+ * @see lithium\net\http\Route::$_template
+ * @see lithium\net\http\Route::$_keys
+ */
protected $_defaults = array();
+ /**
+ * An array of regular expression patterns used in route matching.
+ *
+ * @var array
+ */
protected $_subPatterns = array();
+ /**
+ * An array of parameter names which will persist by default when generating URLs. By default,
+ * the `'controller'` parameter is set to persist, which means that the controller name matched
+ * for a given request will be used to generate all URLs for that request, unless the
+ * `'controller'` parameter is specified in that URL with another value.
+ *
+ * @var array
+ */
+ protected $_persist = array();
+
+ /**
+ * Contains a function which will be executed if this route is matched. The function takes the
+ * instance of the associated `Request` object, and the array of matched route parameters, and
+ * must return either the parameters array (which may be modified by the handler) or a
+ * `Response` object, in which case the response will be returned directly. This may be used to
+ * handle redirects, or simple API services.
+ *
+ * @var object
+ */
+ protected $_handler = null;
+
+ /**
+ * Auto configuration properties. Also used as the list of properties to return when exporting
+ * this `Route` object to an array.
+ *
+ * @see lithium\net\http\Route::export()
+ * @var array
+ */
protected $_autoConfig = array(
- 'template', 'pattern', 'keys', 'params', 'match', 'defaults', 'subPatterns'
+ 'template', 'pattern', 'params', 'match', 'meta',
+ 'keys', 'defaults', 'subPatterns', 'persist', 'handler'
);
public function __construct(array $config = array()) {
$defaults = array(
- 'params' => array(),
+ 'params' => array(),
'template' => '/',
- 'pattern' => '^[\/]*$',
- 'match' => array(),
+ 'pattern' => '',
+ 'match' => array(),
+ 'meta' => array(),
'defaults' => array(),
- 'keys' => array(),
- 'options' => array()
+ 'keys' => array(),
+ 'persist' => array(),
+ 'handler' => null,
);
parent::__construct($config + $defaults);
}
protected function _init() {
parent::_init();
- $this->_pattern = $this->_pattern ?: rtrim($this->_template, '/');
$this->_params += array('action' => 'index');
- $this->compile($this->_config['options']);
+
+ if (!$this->_config['pattern']) {
+ $this->compile();
+ }
+ if ($isKey = isset($this->_keys['controller']) || isset($this->_params['controller'])) {
+ $this->_persist = $this->_persist ?: array('controller');
+ }
}
/**
@@ -120,13 +199,33 @@ class Route extends \lithium\core\Object {
public function parse($request) {
$url = '/' . trim($request->url, '/');
- if (preg_match($this->_pattern, $url, $match)) {
- $match['args'] = isset($match['args']) ? explode('/', $match['args']) : array();
- $result = array_intersect_key($match, $this->_keys) + $this->_params + $this->_defaults;
- $result['action'] = $result['action'] ?: 'index';
- return $result;
+ if (!preg_match($this->_pattern, $url, $match)) {
+ return false;
+ }
+ foreach ($this->_meta as $key => $compare) {
+ $value = $request->get($key);
+
+ if (!($compare == $value || (is_array($compare) && in_array($value, $compare)))) {
+ return false;
+ }
+ }
+
+ if (isset($match['args'])) {
+ $match['args'] = explode('/', $match['args']);
+ }
+ $result = array_intersect_key($match, $this->_keys) + $this->_params + $this->_defaults;
+
+ if (isset($result['action']) && !$result['action']) {
+ $result['action'] = 'index';
+ }
+ $request->params = $result;
+ $request->persist = $this->_persist;
+
+ if ($this->_handler) {
+ $handler = $this->_handler;
+ return $handler($request);
}
- return false;
+ return $request;
}
/**
@@ -148,24 +247,41 @@ class Route extends \lithium\core\Object {
unset($options['?']);
}
- if (array_intersect_key($options, $this->_match) !== $this->_match) {
+ if (!$options = $this->_matchKeys($options)) {
+ return false;
+ }
+ foreach ($this->_subPatterns as $key => $pattern) {
+ if (isset($options[$key]) && !preg_match("/^{$pattern}$/", $options[$key])) {
+ return false;
+ }
+ }
+ return $this->_write($options, $defaults + $this->_defaults + array('args' => '')) . $query;
+ }
+
+ /**
+ * A helper method used by `match()` to verify that options required to match this route are
+ * present in a URL array.
+ *
+ * @see lithium\net\http\Route::match()
+ * @param array $options An array of URL parameters.
+ * @return mixed On success, returns an updated array of options, merged with defaults. On
+ * failure, returns `false`.
+ */
+ protected function _matchKeys($options) {
+ $args = array('args' => 'args');
+
+ if (array_intersect_key($options, $this->_match) != $this->_match) {
return false;
}
if (array_diff_key(array_diff_key($options, $this->_match), $this->_keys) !== array()) {
return false;
}
$options += $this->_defaults;
- $args = array('args' => 'args');
if (array_intersect_key($this->_keys, $options) + $args !== $this->_keys + $args) {
return false;
}
- foreach ($this->_subPatterns as $key => $pattern) {
- if (isset($options[$key]) && !preg_match("/^{$pattern}$/", $options[$key])) {
- return false;
- }
- }
- return $this->_write($options, $defaults + $this->_defaults + array('args' => '')) . $query;
+ return $options;
}
/**
@@ -185,12 +301,9 @@ class Route extends \lithium\core\Object {
}
foreach (array_reverse($options + array('args' => ''), true) as $key => $value) {
- if (isset($this->_subPatterns[$key])) {
- $rpl = "{:{$key}:{$this->_subPatterns[$key]}}";
- } else {
- $rpl = "{:{$key}}";
- }
- $len = - strlen($rpl);
+ $pattern = isset($this->_subPatterns[$key]) ? ":{$this->_subPatterns[$key]}" : '';
+ $rpl = "{:{$key}{$pattern}}";
+ $len = strlen($rpl) * -1;
if ($trimmed && isset($defaults[$key]) && $value == $defaults[$key]) {
if (substr($template, $len) == $rpl) {
@@ -198,8 +311,12 @@ class Route extends \lithium\core\Object {
continue;
}
}
+ if ($value === null) {
+ $template = str_replace("/{$rpl}", '', $template);
+ continue;
+ }
$template = str_replace($rpl, $value, $template);
- $trimmed = ($key == 'args') ? $trimmed : false;
+ $trimmed = ($key == 'args') ? $trimmed : false;
}
return $template;
}
@@ -213,9 +330,8 @@ class Route extends \lithium\core\Object {
*/
public function export() {
$result = array();
- $keys = array('template', 'pattern', 'keys', 'params', 'match', 'defaults', 'subPatterns');
- foreach ($keys as $key) {
+ foreach ($this->_autoConfig as $key) {
$result[$key] = $this->{'_' . $key};
}
return $result;
@@ -228,27 +344,19 @@ class Route extends \lithium\core\Object {
* @param array $options
* @return void
*/
- public function compile(array $options = array()) {
- $defaults = array('wrap' => true, 'compile' => true);
- $options += $defaults;
-
- if (!$options['compile']) {
- $this->_pattern = $options['wrap'] ? '@^' . $this->_pattern . '$@' : $this->_pattern;
- return;
- }
-
+ public function compile() {
$this->_match = $this->_params;
- $this->_pattern = $this->_template;
- $this->_pattern = $options['wrap'] ? '@^' . $this->_pattern . '$@' : $this->_pattern;
+ $this->_pattern = "@^{$this->_template}\$@";
+ $this->_extractMeta();
if ($this->_template === '/' || $this->_template === '') {
+ $this->_pattern = '@^[\/]*$@';
return;
}
- preg_match_all('/(?:\{:(?P<params>[^}]+)\})/', $this->_pattern, $keys);
-
- if (empty($keys['params'])) {
+ if (!$keys = $this->_compilePatterns($this->_pattern)) {
return;
}
+
$shortKeys = array();
$this->_pattern = str_replace('.{', '\.{', $this->_pattern);
@@ -258,7 +366,7 @@ class Route extends \lithium\core\Object {
$this->_keys['args'] = 'args';
}
- foreach ($keys['params'] as $i => $param) {
+ foreach ($keys as $i => $param) {
$paramName = $param;
if (strpos($param, ':')) {
@@ -274,13 +382,48 @@ class Route extends \lithium\core\Object {
$this->_pattern = str_replace("/{:{$param}}", "(?:/{$regex}){$req}", $this->_pattern);
$this->_pattern = str_replace("{:{$param}}", $regex, $this->_pattern);
}
- $shortKeys += $keys['params'];
+ $shortKeys += $keys;
ksort($shortKeys);
$this->_keys = array_combine($shortKeys, $shortKeys);
$this->_defaults = array_intersect_key($this->_params, $this->_keys);
$this->_match = array_diff_key($this->_params, $this->_defaults);
}
+
+ /**
+ * Extracts HTTP method / header parameters from default parameter list.
+ *
+ * @return void
+ */
+ protected function _extractMeta() {
+ foreach ($this->_params as $key => $value) {
+ if (!strpos($key, ':')) {
+ continue;
+ }
+ unset($this->_params[$key]);
+ $this->_meta[$key] = $value;
+ }
+ }
+
+ /**
+ * Parses route template macros down to regular expression named capture groups.
+ *
+ * @param string $pattern The URL pattern to parse.
+ * @return array Returns an array of regular expression capture patterns.
+ */
+ protected function _compilePatterns($pattern) {
+ $repl = array();
+ $replace = function($value) use (&$repl) {
+ $key = ':::' . count($repl) . ':::';
+ $repl[$key] = $value[0];
+ return $key;
+ };
+ $pattern = preg_replace_callback('/\{[0-9,]+\}/', $replace, $pattern);
+
+ preg_match_all('/(?:\{:(?P<params>[^}]+)\})/', $pattern, $keys);
+ $keys = str_replace(array_keys($repl), array_values($repl), join("\n", $keys['params']));
+ return $keys ? explode("\n", $keys) : array();
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/net/http/Router.php b/libraries/lithium/net/http/Router.php
index 4f327ee..4ff7a71 100644
--- a/libraries/lithium/net/http/Router.php
+++ b/libraries/lithium/net/http/Router.php
@@ -2,14 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\net\http;
-use \lithium\util\Inflector;
-use \lithium\util\Collection;
+use lithium\util\Inflector;
+use lithium\util\Collection;
+use lithium\net\http\RoutingException;
/**
* The two primary responsibilities of the `Router` class are to generate URLs from parameter lists,
@@ -20,25 +21,49 @@ use \lithium\util\Collection;
* `app\controllers\UsersController::login()`, you could set up a route like the following in
* `app/config/routes.php`:
* {{{
- * use \lithium\net\http\Router;
+ * use lithium\net\http\Router;
*
* Router::connect('/login', array('controller' => 'users', 'action' => 'login'));}}}
*
* Not only would that correctly route all requests for `/login` to `UsersController::index()`, but
* any time the framework generated a route with matching parameters, `Router` would return the
- * correct short URL. This allows you to keep your application's URL structure nicely decoupled
- * from the underlying software design.
+ * correct short URL.
*
- * For more information on parsing and generating URLs, see the `parse()` and `match()` methods.
+ * While most framework components that work with URLs (and utilize routing) handle calling the
+ * `Router` directly (i.e. controllers doing redirects, or helpers generating links), if you have a
+ * scenario where you need to call the `Router` directly, you can use the `match()` method.
+ *
+ * This allows you to keep your application's URL structure nicely decoupled from the underlying
+ * software design. For more information on parsing and generating URLs, see the `parse()` and
+ * `match()` methods.
*/
class Router extends \lithium\core\StaticObject {
+ /**
+ * An array of loaded lithium\net\http\Route objects used to match Request objects against.
+ *
+ * @var array
+ */
protected static $_configurations = array();
+ /**
+ * Classes used by `Router`.
+ *
+ * @var array
+ */
protected static $_classes = array(
- 'route' => '\lithium\net\http\Route'
+ 'route' => 'lithium\net\http\Route'
);
+ public static function config($config = array()) {
+ if (!$config) {
+ return array('classes' => static::$_classes);
+ }
+ if (isset($config['classes'])) {
+ static::$_classes = $config['classes'] + static::$_classes;
+ }
+ }
+
/**
* Connects a new route and returns the current routes array. This method creates a new
* `Route` object and registers it with the `Router`. The order in which routes are connected
@@ -53,16 +78,41 @@ class Router extends \lithium\core\StaticObject {
* @param array $options
* @return array Array of routes
*/
- public static function connect($template, $params = array(), array $options = array()) {
+ public static function connect($template, $params = array(), $options = array()) {
if (!is_object($template)) {
- $params + array('action' => 'index');
+ if (is_string($params)) {
+ $params = static::_parseString($params, false);
+ }
+ if (isset($params[0]) && is_array($tmp = static::_parseString($params[0], false))) {
+ unset($params[0]);
+ $params = $tmp + $params;
+ }
+ $params += array('action' => 'index');
+
+ if (is_callable($options)) {
+ $options = array('handler' => $options);
+ }
$class = static::$_classes['route'];
- $template = new $class(compact('template', 'params', 'options'));
+ $template = new $class(compact('template', 'params') + $options);
}
return (static::$_configurations[] = $template);
}
/**
+ * Wrapper method which takes a `Request` object, parses it through all attached `Route`
+ * objects, and assigns the resulting parameters to the `Request` object, and returning it.
+ *
+ * @param object $request A request object, usually an instance of `lithium\action\Request`.
+ * @return object Returns a copy of the `Request` object with parameters applied.
+ */
+ public static function process($request) {
+ if (!$result = static::parse($request)) {
+ return $request;
+ }
+ return $result;
+ }
+
+ /**
* Accepts an instance of `lithium\action\Request` (or a subclass) and matches it against each
* route, in the order that the routes are connected.
*
@@ -83,43 +133,154 @@ class Router extends \lithium\core\StaticObject {
/**
* Attempts to match an array of route parameters (i.e. `'controller'`, `'action'`, etc.)
- * against a connected `Route` object.
+ * against a connected `Route` object. For example, given the following route:
*
- * @param array $options
- * @param object $context
- * @return string
+ * {{{
+ * Router::connect('/login', array('controller' => 'users', 'action' => 'login'));
+ * }}}
+ *
+ * This will match:
+ * {{{
+ * $url = Router::match(array('controller' => 'users', 'action' => 'login'));
+ * // returns /login
+ * }}}
+ *
+ * For URLs templates with no insert parameters (i.e. elements like `{:id}` that are replaced
+ * with a value), all parameters must match exactly as they appear in the route parameters.
+ *
+ * Alternatively to using a full array, you can specify routes using a more compact syntax. The
+ * above example can be written as:
+ *
+ * {{{ $url = Router::match('User::login'); // still returns /login }}}
+ *
+ * You can combine this with more complicated routes; for example:
+ * {{{
+ * Router::connect('/posts/{:id:\d+}', array('controller' => 'posts', 'action' => 'view'));
+ * }}}
+ *
+ * This will match:
+ * {{{
+ * $url = Router::match(array('controller' => 'posts', 'action' => 'view', 'id' => '1138'));
+ * // returns /posts/1138
+ * }}}
+ *
+ * Again, you can specify the same URL with a more compact syntax, as in the following:
+ * {{{
+ * $url = Router::match(array('Posts::view', 'id' => '1138'));
+ * // again, returns /posts/1138
+ * }}}
+ *
+ * You can use either syntax anywhere a URL is accepted, i.e.
+ * `lithium\action\Controller::redirect()`, or `lithium\template\helper\Html::link()`.
+ *
+ * @param string|array $url Options to match to a URL. Optionally, this can be a string
+ * containing a manually generated URL.
+ * @param object $context An instance of `lithium\action\Request`. This supplies the context for
+ * any persistent parameters, as well as the base URL for the application.
+ * @param array $options Options for the generation of the matched URL. Currently accepted
+ * values are:
+ * - `'absolute'` _boolean_: Indicates whether or not the returned URL should be an
+ * absolute path (i.e. including scheme and host name).
+ * - `'host'` _string_: If `'absolute'` is `true`, sets the host name to be used,
+ * or overrides the one provided in `$context`.
+ * - `'scheme'` _string_: If `'absolute'` is `true`, sets the URL scheme to be
+ * used, or overrides the one provided in `$context`.
+ * @return string Returns a generated URL, based on the URL template of the matched route, and
+ * prefixed with the base URL of the application.
*/
- public static function match($options = array(), $context = null) {
- if (is_string($options)) {
- $path = $options;
-
- if (strpos($path, '#') === 0 || strpos($path, 'mailto') === 0 || strpos($path, '://')) {
- return $path;
+ public static function match($url = array(), $context = null, array $options = array()) {
+ if (is_string($url)) {
+ if (strpos($url, '#') === 0 || strpos($url, 'mailto') === 0 || strpos($url, '://')) {
+ return $url;
}
-
- if (!preg_match('/^[A-Za-z0-9_]+::[A-Za-z0-9_]+$/', $path)) {
- $base = $context ? $context->env('base') : '';
- $path = trim($path, '/');
- return "{$base}/{$path}";
+ if (is_string($url = static::_parseString($url, $context))) {
+ return static::_prefix($url, $context, $options);
}
- list($controller, $action) = explode('::', $path, 2);
- $controller = Inflector::underscore($controller);
- $options = compact('controller', 'action');
}
- $defaults = array_filter(array(
- 'action' => ($context && $context->action) ? $context->action : 'index',
- 'controller' => ($context && $context->action) ? $context->controller : null
- ));
- $options += $defaults;
+ if (isset($url[0]) && is_array($params = static::_parseString($url[0], $context))) {
+ unset($url[0]);
+ $url = $params + $url;
+ }
+ $url = static::_persist($url, $context);
+ $defaults = array('action' => 'index');
+ $url += $defaults;
+
$base = isset($context) ? $context->env('base') : '';
+ $suffix = isset($url['#']) ? "#{$url['#']}" : null;
+ unset($url['#']);
foreach (static::$_configurations as $route) {
- if ($match = $route->match($options, $context)) {
- return "{$base}{$match}";
+ if (!$match = $route->match($url, $context)) {
+ continue;
+ }
+ $path = rtrim("{$base}{$match}{$suffix}", '/') ?: '/';
+ $path = ($options) ? static::_prefix($path, $context, $options) : $path;
+ return $path ?: '/';
+ }
+ $match = array("\n", 'array (', ',)', '=> NULL', '( \'', ', ');
+ $replace = array('', '(', ')', '=> null', '(\'', ', ');
+ $url = str_replace($match, $replace, var_export($url, true));
+ throw new RoutingException("No parameter match found for URL `{$url}`.");
+ }
+
+ /**
+ * Returns the prefix (scheme + hostname) for a URL based on the passed `$options` and the
+ * `$context`.
+ *
+ * @param string $path The URL to be prefixed.
+ * @param object $context The request context.
+ * @param array $options Options for generating the proper prefix. Currently accepted values
+ * are: `'absolute' => true|false`, `'host' => string` and `'scheme' => string`.
+ * @return string The prefixed URL, depending on the passed options.
+ */
+ protected static function _prefix($path, $context = null, array $options = array()) {
+ $defaults = array('scheme' => null, 'host' => null, 'absolute' => false);
+
+ if ($context) {
+ $defaults['host'] = $context->env('HTTP_HOST');
+ $defaults['scheme'] = $context->env('HTTPS') ? 'https://' : 'http://';
+ }
+ $options += $defaults;
+
+ return ($options['absolute']) ? "{$options['scheme']}{$options['host']}{$path}" : $path;
+ }
+
+ /**
+ * Copies persistent parameters (parameters in the request which have been designated to
+ * persist) to the current URL, unless the parameter has been explicitly disabled from
+ * persisting by setting the value in the URL to `null`, or by assigning some other value.
+ *
+ * For example:
+ *
+ * {{{ embed:lithium\tests\cases\net\http\RouterTest::testParameterPersistence(1-10) }}}
+ *
+ * @see lithium\action\Request::$persist
+ * @param array $url The parameters that define the URL to be matched.
+ * @param object $context Typically an instance of `lithium\action\Request`, which contains a
+ * `$persist` property, which is an array of keys to be persisted in URLs between
+ * requests.
+ * @return array Returns the modified URL array.
+ */
+ protected static function _persist($url, $context) {
+ if (!$context || !isset($context->persist)) {
+ return $url;
+ }
+ foreach ($context->persist as $key) {
+ $url += array($key => $context->params[$key]);
+
+ if ($url[$key] === null) {
+ unset($url[$key]);
}
}
+ return $url;
}
+ /**
+ * Returns a route from the loaded configurations, by name.
+ *
+ * @param string $route Name of the route to request.
+ * @return lithium\net\http\Route
+ */
public static function get($route = null) {
if ($route === null) {
return static::$_configurations;
@@ -135,6 +296,24 @@ class Router extends \lithium\core\StaticObject {
public static function reset() {
static::$_configurations = array();
}
+
+ /**
+ * Helper function for taking a path string and parsing it into a controller and action array.
+ *
+ * @param string $path Path string to parse.
+ * @param boolean $context
+ * @return array
+ */
+ protected static function _parseString($path, $context) {
+ if (!preg_match('/^[A-Za-z0-9_]+::[A-Za-z0-9_]+$/', $path)) {
+ $base = $context ? $context->env('base') : '';
+ $path = trim($path, '/');
+ return $context !== false ? "{$base}/{$path}" : null;
+ }
+ list($controller, $action) = explode('::', $path, 2);
+ $controller = Inflector::underscore($controller);
+ return compact('controller', 'action');
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/net/http/RoutingException.php b/libraries/lithium/net/http/RoutingException.php
new file mode 100644
index 0000000..ac472ff
--- /dev/null
+++ b/libraries/lithium/net/http/RoutingException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\net\http;
+
+/**
+ * A `RoutingException` is thrown whenever a the `Router` cannot match a set of parameters against
+ * the available collection of attached routes.
+ */
+class RoutingException extends \RuntimeException {
+
+ protected $code = 500;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/net/http/Service.php b/libraries/lithium/net/http/Service.php
index 2e5823b..c98acad 100644
--- a/libraries/lithium/net/http/Service.php
+++ b/libraries/lithium/net/http/Service.php
@@ -2,12 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
+
namespace lithium\net\http;
-use \lithium\core\Libraries;
+use lithium\core\Libraries;
+use lithium\core\ClassNotFoundException;
/**
* Basic Http Service.
@@ -16,7 +18,14 @@ use \lithium\core\Libraries;
class Service extends \lithium\core\Object {
/**
- * Holds the request and response used by send.
+ * The `Socket` instance used to send `Service` calls.
+ *
+ * @var lithium\net\Socket
+ */
+ public $connection = null;
+
+ /**
+ * Holds the last request and response object
*
* @var object
*/
@@ -30,13 +39,6 @@ class Service extends \lithium\core\Object {
protected $_autoConfig = array('classes' => 'merge');
/**
- * The `Socket` instance used to send `Service` calls.
- *
- * @var \lithium\net\Socket
- */
- protected $_connection = null;
-
- /**
* Indicates whether `Service` can connect to the HTTP endpoint for which it is configured.
* Defaults to true until a connection attempt fails.
*
@@ -50,10 +52,9 @@ class Service extends \lithium\core\Object {
* @var array
*/
protected $_classes = array(
- 'media' => '\lithium\net\http\Media',
- 'request' => '\lithium\net\http\Request',
- 'response' => '\lithium\net\http\Response',
- 'socket' => '\lithium\net\socket\Context'
+ 'media' => 'lithium\net\http\Media',
+ 'request' => 'lithium\net\http\Request',
+ 'response' => 'lithium\net\http\Response',
);
/**
@@ -65,58 +66,28 @@ class Service extends \lithium\core\Object {
*/
public function __construct(array $config = array()) {
$defaults = array(
- 'autoConnect' => true,
'persistent' => false,
- 'protocol' => 'http',
+ 'scheme' => 'http',
'host' => 'localhost',
- 'version' => '1.1',
- 'auth' => 'Basic',
- 'login' => 'root',
- 'password' => '',
- 'port' => 80,
- 'timeout' => 1,
+ 'port' => null,
+ 'timeout' => 30,
+ 'auth' => null,
+ 'username' => null,
+ 'password' => null,
'encoding' => 'UTF-8',
+ 'socket' => 'Context',
);
- $config = (array) $config + $defaults;
-
- $config['auth'] = array(
- 'method' => $config['auth'],
- 'username' => $config['login'],
- 'password' => $config['password']
- );
- parent::__construct($config);
- }
-
- protected function _init() {
- parent::_init();
- $class = Libraries::locate('socket.util', $this->_classes['socket']);
- if (is_string($class)) {
- $this->_connection = new $class($this->_config);
- }
+ parent::__construct($config + $defaults);
}
/**
- * Connect to data source.
+ * Send HEAD request.
*
- * @return boolean
- */
- public function connect() {
- if (!$this->_isConnected && $this->_connection) {
- $this->_isConnected = $this->_connection->open();
- }
- return $this->_isConnected;
- }
-
- /**
- * Disconnect from socket.
- *
- * @return boolean
+ * @param array $options
+ * @return string
*/
- public function disconnect() {
- if ($this->_isConnected) {
- $this->_isConnected = !$this->_connection->close();;
- }
- return !$this->_isConnected;
+ public function head(array $options = array()) {
+ return $this->send(__FUNCTION__, null, array(), $options);
}
/**
@@ -168,29 +139,46 @@ class Service extends \lithium\core\Object {
}
/**
+ * Retrieve instance of configured socket
+ *
+ * @param array $config options to be passed on to the socket
+ * @return object
+ */
+ public function &connection($config = array()) {
+ $config += $this->_config;
+
+ try {
+ $this->connection = Libraries::instance('socket', $config['socket'], $config);
+ } catch (ClassNotFoundException $e) {
+ $this->connection = null;
+ }
+ return $this->connection;
+ }
+
+ /**
* Send request and return response data.
*
* @param string $method
* @param string $path
- * @param array $data
- * @param array $options
+ * @param array $data the parameters for the request. For GET/DELETE this is the query string
+ * for POST/PUT this is the body
+ * @param array $options passed to request and socket
* @return string
*/
- public function send($method, $path = null, $data = null, array $options = array()) {
- $defaults = array('return' => 'body');
- $options += $defaults;
+ public function send($method, $path = null, $data = array(), array $options = array()) {
+ $defaults = array('return' => 'body', 'classes' => $this->_classes);
+ $options += $defaults + $this->_config;
+ $request = $this->_request($method, $path, $data, $options);
+ $options += array('message' => $request);
- if (!$this->connect()) {
+ if (!($conn =& $this->connection($options)) || !$conn->open()) {
return;
}
- $request = $this->_request($method, $path, $data, $options);
- $response = $this->_connection->send($request, array('classes' => $this->_classes));
- if ($response) {
- $this->last = (object) compact('request', 'response');
- $this->disconnect();
- return ($options['return'] == 'body') ? $response->body() : $response;
- }
+ $response = $conn->send($request, $options);
+ $conn->close();
+ $this->last = (object) compact('request', 'response');
+ return ($options['return'] == 'body' && $response) ? $response->body() : $response;
}
/**
@@ -207,10 +195,12 @@ class Service extends \lithium\core\Object {
*/
protected function _request($method, $path, $data, $options) {
$defaults = array('type' => 'form');
- $options += $defaults;
- $request = new $this->_classes['request']($this->_config + $options);
+ $options += $defaults + $this->_config;
+
+ $request = $this->_instance('request', $options);
$request->path = str_replace('//', '/', "{$request->path}{$path}");
$request->method = $method = strtoupper($method);
+
$media = $this->_classes['media'];
$type = null;
@@ -220,7 +210,8 @@ class Service extends \lithium\core\Object {
$request->headers(array('Content-Type' => current($contentType)));
$data = Media::encode($options['type'], $data, $options);
}
- in_array($method, array('POST', 'PUT')) ? $request->body($data) : $request->params = $data;
+ in_array($method, array('POST', 'PUT'))
+ ? $request->body($data) : $request->params = $data;
return $request;
}
}
diff --git a/libraries/lithium/net/socket/Context.php b/libraries/lithium/net/socket/Context.php
index 75fd64a..8c5d43f 100644
--- a/libraries/lithium/net/socket/Context.php
+++ b/libraries/lithium/net/socket/Context.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -13,68 +13,131 @@ namespace lithium\net\socket;
*/
class Context extends \lithium\net\Socket {
- protected $_connection = null;
+ /**
+ * Connection timeout value.
+ *
+ * @var integer
+ */
+ protected $_timeout = 30;
+
+ /**
+ * Content of the stream
+ *
+ * @var string
+ */
+ protected $_content = null;
+ /**
+ * Constructor
+ *
+ * @param array $config
+ */
+ public function __construct(array $config = array()) {
+ $defaults = array('mode' => 'r', 'message' => null);
+ parent::__construct($config + $defaults);
+ $this->timeout($this->_config['timeout']);
+ }
+
+ /**
+ * Opens the socket and sets its timeout value.
+ *
+ * @return boolean Success.
+ */
public function open() {
- return true;
+ $config = $this->_config;
+ $url = "{$config['scheme']}://{$config['host']}:{$config['port']}";
+ $context = array($config['scheme'] => array('timeout' => $this->_timeout));
+
+ if (is_object($config['message'])) {
+ $url = $config['message']->to('url');
+ $context = $config['message']->to('context', array('timeout' => $this->_timeout));
+ }
+ $this->_resource = fopen($url, $config['mode'], false, stream_context_create($context));
+ return $this->_resource;
}
+ /**
+ * Closes the socket connection.
+ *
+ * @return boolean Success.
+ */
public function close() {
- if (is_resource($this->_connection)) {
- return fclose($this->_connection);
+ if (!is_resource($this->_resource)) {
+ return true;
+ }
+ fclose($this->_resource);
+ if (is_resource($this->_resource)) {
+ $this->close();
}
return true;
}
+ /**
+ * End of file test for this socket connection. Does not apply to this implementation.
+ *
+ * @return boolean Success.
+ */
public function eof() {
- return true;
+ if (!is_resource($this->_resource)) {
+ return true;
+ }
+ return feof($this->_resource);
}
+ /**
+ * Reads from the socket. Does not apply to this implementation.
+ *
+ * @return void
+ */
public function read() {
- return null;
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ $meta = stream_get_meta_data($this->_resource);
+ $headers = isset($meta['wrapper_data'])
+ ? join("\r\n", $meta['wrapper_data']) . "\r\n\r\n" : null;
+ return $headers . stream_get_contents($this->_resource);
}
- public function write($data) {
- return true;
+ /**
+ * Writes to the socket.
+ *
+ * @param string $data Data to write.
+ * @return boolean Success
+ */
+ public function write($data = null) {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ if (!is_object($data)) {
+ $data = $this->_instance($this->_classes['request'], (array) $data + $this->_config);
+ }
+ return stream_context_set_option(
+ $this->_resource, $data->to('context', array('timeout' => $this->_timeout))
+ );
}
+ /**
+ * Sets the timeout on the socket *connection*.
+ *
+ * @param integer $time Seconds after the connection times out.
+ * @return booelan `true` if timeout has been set, `false` otherwise.
+ */
public function timeout($time = null) {
- return true;
- }
-
- public function encoding($encoding = null) {
- return false;
+ if ($time !== null) {
+ $this->_timeout = $time;
+ }
+ return $this->_timeout;
}
/**
- * Send request and return response data
+ * Sets the encoding of the socket connection. Does not apply to this implementation.
*
- * @param string $message
- * @param array $options
- * @return string
+ * @param string $charset The character set to use.
+ * @return boolean `true` if encoding has been set, `false` otherwise.
*/
- public function send($message, array $options = array()) {
- $defaults = array('path' => null, 'classes' => array('response' => null));
- $options += $defaults;
-
- if ($this->open() === false) {
- return false;
- }
- $url = is_object($message) ? $message->to('url') : $options['path'];
- $message = is_object($message) ? $message->to('context') : $message;
-
- if ($this->_connection = fopen($url, 'r', false, stream_context_create($message))) {
- $meta = stream_get_meta_data($this->_connection);
- $headers = $meta['wrapper_data'] ?: array();
- $message = isset($headers[0]) ? $headers[0] : null;
- $body = stream_get_contents($this->_connection);
- $this->close();
-
- if (!$options['classes']['response']) {
- return $body;
- }
- return new $options['classes']['response'](compact('headers', 'body', 'message'));
- }
+ public function encoding($charset = null) {
+ return false;
}
}
diff --git a/libraries/lithium/net/socket/Curl.php b/libraries/lithium/net/socket/Curl.php
index 328eaaa..9dd6c1b 100644
--- a/libraries/lithium/net/socket/Curl.php
+++ b/libraries/lithium/net/socket/Curl.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -18,51 +18,61 @@ namespace lithium\net\socket;
* is not the case, you must either recompile PHP with the proper configuration flags to enable
* curl, or you may use the `Stream` adapter that is also included with the Lithium core.
*
- * @see http://www.php.net/manual/en/curl.installation.php
+ * @link http://www.php.net/manual/en/curl.installation.php
* @see lithium\net\socket\Stream
*/
class Curl extends \lithium\net\Socket {
/**
- * Contains options that will be passed to curl_setopt_array before
+ * Contains options that will be passed to `curl_setopt_array` before
* `read` and `write` operations. These options should be set by
* using the `set` method.
*
- * @var array
- * @see http://www.php.net/manual/en/function.curl-setopt.php
+ * @link http://www.php.net/manual/en/function.curl-setopt.php PHP Manual: curl_setopt()
* @see lithium\net\socket\Curl::set()
+ * @var array
*/
public $options = array();
/**
- * Opens a curl connection and initializes the internal resource handle
+ * Constructor
*
- * @return mixed False if the Socket configuration does not contain the
- * 'protocol' or 'host' settings, curl resource otherwise.
+ * @param array $config
+ */
+ public function __construct(array $config = array()) {
+ $defaults = array('ignoreExpect' => true);
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Opens a curl connection and initializes the internal resource handle.
+ *
+ * @return mixed Returns `false` if the socket configuration does not contain the
+ * `'scheme'` or `'host'` settings, or if configuration fails, otherwise returns a
+ * curl resource.
*/
public function open() {
$config = $this->_config;
- if (empty($config['protocol']) || empty($config['host'])) {
+ if (empty($config['scheme']) || empty($config['host'])) {
return false;
}
- $url = "{$config['protocol']}://{$config['host']}";
+ $url = "{$config['scheme']}://{$config['host']}";
$this->_resource = curl_init($url);
curl_setopt($this->_resource, CURLOPT_PORT, $config['port']);
- curl_setopt($this->_resource, CURLOPT_HEADER, 0);
+ curl_setopt($this->_resource, CURLOPT_HEADER, true);
curl_setopt($this->_resource, CURLOPT_RETURNTRANSFER, true);
- if (is_resource($this->_resource)) {
- $this->_isConnected = true;
-
- $this->timeout($config['timeout']);
-
- if (!empty($config['encoding'])) {
- $this->encoding($config['encoding']);
- }
+ if (!is_resource($this->_resource)) {
+ return false;
}
+ $this->_isConnected = true;
+ $this->timeout($config['timeout']);
+ if (isset($config['encoding'])) {
+ $this->encoding($config['encoding']);
+ }
return $this->_resource;
}
@@ -95,37 +105,48 @@ class Curl extends \lithium\net\Socket {
* Reads data from the curl connection.
* The `read` method will utilize the curl options that have been set.
*
+ * @link http://php.net/manual/en/function.curl-exec.php PHP Manual: curl_exec()
* @return mixed Boolean false if the resource handle is unavailable, and the result
* of `curl_exec` otherwise.
- * @see http://php.net/manual/en/function.curl-exec.php
*/
public function read() {
if (!is_resource($this->_resource)) {
return false;
}
- curl_setopt_array($this->_resource, $this->options);
return curl_exec($this->_resource);
}
/**
- * Reads data from the curl connection.
- * The `read` method will utilize the curl options that have been set.
+ * Writes data to curl options
*
- * @param array $data
- * @return mixed Boolean false if the resource handle is unavailable, and the result
- * of `curl_exec` otherwise.
- * @see http://php.net/manual/en/function.curl-exec.php
+ * @param object $data a `lithium\net\Message` object or array
+ * @return boolean
*/
- public function write($data) {
+ public function write($data = null) {
if (!is_resource($this->_resource)) {
return false;
}
- curl_setopt_array($this->_resource, $this->options);
- return curl_exec($this->_resource);
+ if (!is_object($data)) {
+ $data = $this->_instance($this->_classes['request'], (array) $data + $this->_config);
+ }
+ $this->set(CURLOPT_URL, $data->to('url'));
+
+ if (is_a($data, 'lithium\net\http\Message')) {
+ if (!empty($this->_config['ignoreExpect'])) {
+ $data->headers('Expect', ' ');
+ }
+ if (isset($data->headers)) {
+ $this->set(CURLOPT_HTTPHEADER, $data->headers());
+ }
+ if (isset($data->method) && $data->method == 'POST') {
+ $this->set(array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data->body()));
+ }
+ }
+ return (boolean) curl_setopt_array($this->_resource, $this->options);
}
/**
- * A convenience method to set the curl CURLOPT_CONNECTTIMEOUT
+ * A convenience method to set the curl `CURLOPT_CONNECTTIMEOUT`
* setting for the current connection. This determines the number
* of seconds to wait while trying to connect.
*
@@ -148,18 +169,17 @@ class Curl extends \lithium\net\Socket {
* @todo implement Curl::encoding($charset)
* @param string $charset
*/
- public function encoding($charset) {
- }
-
+ public function encoding($charset) {}
/**
* Sets the options to be used in subsequent curl requests.
*
+ * @link http://www.php.net/manual/en/curl.constants.php PHP Manual: cURL Constants
* @param array $flags If $values is an array, $flags will be used as the
* keys to an associative array of curl options. If $values is not set,
* then $flags will be used as the associative array.
* @param array $value If set, this array becomes the values for the
* associative array of curl options.
- * @see http://www.php.net/manual/en/curl.constants.php for valid option constants
+ * @return void
*/
public function set($flags, $value = null) {
if ($value !== null) {
@@ -167,24 +187,6 @@ class Curl extends \lithium\net\Socket {
}
$this->options += $flags;
}
-
- /**
- * Aggregates read and write methods into a coherent request response
- *
- * @param mixed $message array or object like `\lithium\net\http\Request`
- * @param array $options
- * - path: path for the current request
- * - classes: array of classes to use
- * - response: a class to use for the response
- * @return boolean response string or object like `\lithium\net\http\Response`
- */
- public function send($message, array $options = array()) {
- if ($this->write((string) $message)) {
- $message = $this->read();
- $response = new $options['classes']['response'](compact('message'));
- return $response;
- }
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/net/socket/Stream.php b/libraries/lithium/net/socket/Stream.php
index 658f70b..881d968 100644
--- a/libraries/lithium/net/socket/Stream.php
+++ b/libraries/lithium/net/socket/Stream.php
@@ -2,21 +2,21 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\net\socket;
-use \Exception;
+use lithium\core\NetworkException;
/**
- * A PHP stream-based socket adapter
+ * A PHP stream-based socket adapter.
*
- * This stream adapter provides the required method implementations of the abstract Socket class
- * for `open`, `close`, `read`, `write`, `timeout` `eof` and `encoding`.
+ * This stream adapter provides the required method implementations of the abstract `Socket` class
+ * for the `open()`, `close()`, `read()`, `write()`, `timeout()` `eof()` and `encoding()` methods.
*
- * @see http://www.php.net/manual/en/book.stream.php
+ * @link http://www.php.net/manual/en/book.stream.php PHP Manual: Streams
* @see lithium\net\socket\Stream
*/
class Stream extends \lithium\net\Socket {
@@ -30,11 +30,12 @@ class Stream extends \lithium\net\Socket {
public function open() {
$config = $this->_config;
- if (empty($config['protocol']) || empty($config['host'])) {
+ if (!$config['scheme'] || !$config['host']) {
return false;
}
-
- $host = "{$config['protocol']}://{$config['host']}:{$config['port']}";
+ $scheme = ($config['scheme'] !== 'udp') ? 'tcp' : 'udp';
+ $port = $config['port'] ?: 80;
+ $host = "{$scheme}://{$config['host']}:{$port}";
$flags = STREAM_CLIENT_CONNECT;
if ($config['persistent']) {
@@ -44,16 +45,14 @@ class Stream extends \lithium\net\Socket {
$host, $errorCode, $errorMessage, $config['timeout'], $flags
);
- if (!empty($errorCode) || !empty($errorMessage)) {
- throw new Exception($errorMessage, $errorCode);
+ if ($errorCode || $errorMessage) {
+ throw new NetworkException($errorMessage);
}
-
$this->timeout($config['timeout']);
if (!empty($config['encoding'])) {
$this->encoding($config['encoding']);
}
-
return $this->_resource;
}
@@ -67,6 +66,7 @@ class Stream extends \lithium\net\Socket {
return true;
}
fclose($this->_resource);
+
if (is_resource($this->_resource)) {
$this->close();
}
@@ -76,13 +76,10 @@ class Stream extends \lithium\net\Socket {
/**
* Determines if the socket resource is at EOF.
*
- * @return boolean True if resource pointer is at EOF, false otherwise.
+ * @return boolean Returns `true` if resource pointer is at its EOF, `false` otherwise.
*/
public function eof() {
- if (!is_resource($this->_resource)) {
- return true;
- }
- return feof($this->_resource);
+ return is_resource($this->_resource) ? feof($this->_resource) : true;
}
/**
@@ -97,9 +94,10 @@ class Stream extends \lithium\net\Socket {
if (!is_resource($this->_resource)) {
return false;
}
- return is_null($length) ? stream_get_contents($this->_resource) : stream_get_contents(
- $this->_resource, $length, $offset
- );
+ if (!$length) {
+ return stream_get_contents($this->_resource);
+ }
+ return stream_get_contents($this->_resource, $length, $offset);
}
/**
@@ -108,19 +106,23 @@ class Stream extends \lithium\net\Socket {
* @param string $data The string to be written.
* @return mixed False on error, number of bytes written otherwise.
*/
- public function write($data) {
+ public function write($data = null) {
if (!is_resource($this->_resource)) {
return false;
}
- return fwrite($this->_resource, $data, strlen($data));
+ if (!is_object($data)) {
+ $data = $this->_instance($this->_classes['request'], (array) $data + $this->_config);
+ }
+ return fwrite($this->_resource, (string) $data, strlen((string) $data));
}
/**
- * Set timeout period on a stream
+ * Set timeout period on a stream.
*
+ * @link http://www.php.net/manual/en/function.stream-set-timeout.php
+ * PHP Manual: stream_set_timeout()
* @param integer $time The timeout value in seconds.
* @return void
- * @see http://www.php.net/manual/en/function.stream-set-timeout.php
*/
public function timeout($time) {
if (!is_resource($this->_resource)) {
@@ -133,10 +135,11 @@ class Stream extends \lithium\net\Socket {
* Sets the character set for stream encoding
*
* Note: This function only exists in PHP 6. For PHP < 6, this method will return void.
+ *
+ * @link http://www.php.net/manual/en/function.stream-encoding.php stream_encoding()
* @param string $charset
- * @return mixed Returns void if `stream_encoding` method does not exist, boolean
- * result of `stream_encoding` otherwise.
- * @see http://www.php.net/manual/en/function.stream-encoding.php
+ * @return mixed Returns `null` if `stream_encoding()` function does not exist, boolean
+ * result of `stream_encoding()` otherwise.
*/
public function encoding($charset) {
if (!function_exists('stream_encoding')) {
@@ -144,24 +147,6 @@ class Stream extends \lithium\net\Socket {
}
return is_resource($this->_resource) ? stream_encoding($this->_resource, $charset) : false;
}
-
- /**
- * Aggregates read and write methods into a coherent request response
- *
- * @param mixed $message array or object like `\lithium\net\http\Request`
- * @param array $options
- * - path: path for the current request
- * - classes: array of classes to use
- * - response: a class to use for the response
- * @return boolean response string or object like `\lithium\net\http\Response`
- */
- public function send($message, array $options = array()) {
- if ($this->write((string) $message)) {
- $message = $this->read();
- $response = new $options['classes']['response'](compact('message'));
- return $response;
- }
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/readme.wiki b/libraries/lithium/readme.wiki
new file mode 100755
index 0000000..b3e561d
--- /dev/null
+++ b/libraries/lithium/readme.wiki
@@ -0,0 +1,29 @@
+#### You asked for a better framework. Here it is.
+
+Lithium is the fast, flexible and most RAD development framework for PHP 5.3 and up.
+
+##### A framework of firsts
+
+Lithium is the first and only major PHP framework built from the ground up for PHP 5.3+, and the first to break ground into major new technologies, including bridging the gap between relational and non-relational databases through a single, unified API.
+
+##### Promiscuously opinionated
+
+Some frameworks give you a solid set of classes, but little or no default project organization, leaving you to fend for yourself on each project you create, and spend time wiring up framework classes that should just work together. Others provide you with great organizational conventions, but no way to break out of those conventions if you need to, and too often, no way to override or replace core framework classes.
+
+Lithium is the first framework to give you the best of both worlds, without compromising either. In fact, Lithium's API is intentionally designed to allow you to "grow out of" the framework and into your own custom code over the course of your application's lifecycle, if your needs require.
+
+#### Technology
+
+Lithium takes full advantage of the latest PHP 5.3 features, including namespaces, late static binding and closures. Lithium's innovative [method filter system](/docs/lithium/util/collection/Filters) makes extensive use of closures and anonymous functions to allow application developers to "wrap" framework method calls, intercepting parameters before, and return values after.
+
+Lithium also complies with the PHP 5.3 namespacing standard, allowing you to easily integrate other PHP 5.3 standard libraries and frameworks with Lithium applications, and vice-versa.
+
+Lithium integrates the latest storage technologies, including MongoDB, CouchDB and Redis, with plugin support for Cassandra, ElasticSearch and others.
+
+#### Flexibility
+
+Lithium gives you full control over your application, from filters to dynamically modify framework internals, to dynamic dependencies to extend and replace core classes with application or plugin classes, to heavy use of adapter-oriented configurations, to make it seamless to move between different technologies and options.
+
+Every component of the Lithium framework stack is replaceable through the robust plugin architecture. Swap out the default ORM / ODM implementation for [Doctrine 2](http://dev.lithify.me/li3_doctrine/) or [PHP ActiveRecord](http://dev.lithify.me/li3_activerecord). Don't like the templating? Use [ Twig](http://dev.lithify.me/li3_twig), [ Mustache](https://github.com/bobthecow/mustache.php), or roll your own.
+
+If you don't even need to write a full application, build a micro-app in a single file using the routing system, without giving up the maintainability of the framework's structure.
\ No newline at end of file
diff --git a/libraries/lithium/security/Auth.php b/libraries/lithium/security/Auth.php
index 4e1f6cd..b9019a2 100644
--- a/libraries/lithium/security/Auth.php
+++ b/libraries/lithium/security/Auth.php
@@ -2,12 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\security;
+use lithium\core\ConfigException;
+
/**
* The `Auth` class provides a common interface to authenticate user credentials from different
* sources against different storage backends in a common way. As with most other adapter-driven
@@ -33,7 +35,7 @@ class Auth extends \lithium\core\Adaptable {
*
* @var object `Collection` of authentication configurations.
*/
- protected static $_configurations;
+ protected static $_configurations = array();
/**
* Libraries::locate() compatible path to adapters for this class.
@@ -109,6 +111,10 @@ class Auth extends \lithium\core\Adaptable {
return static::_filter(__FUNCTION__, $params, function($self, $params) {
extract($params);
$config = $self::invokeMethod('_config', array($name));
+
+ if ($config === null) {
+ throw new ConfigException("Configuration `{$name}` has not been defined.");
+ }
$session = $config['session'];
if ($options['checkSession']) {
diff --git a/libraries/lithium/security/auth/adapter/Form.php b/libraries/lithium/security/auth/adapter/Form.php
index cdbdc09..fa9cd91 100644
--- a/libraries/lithium/security/auth/adapter/Form.php
+++ b/libraries/lithium/security/auth/adapter/Form.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\security\auth\adapter;
-use \lithium\core\Libraries;
+use lithium\core\Libraries;
/**
* The `Form` adapter provides basic authentication facilities for checking credentials submitted
@@ -19,16 +19,17 @@ use \lithium\core\Libraries;
* apply any filters as appropriate (see the `'filters'` configuration setting below), and
* query a model class using using the filtered data.
*
- * By default, the adapter uses a model called `User`, and lookup fields called `'username'` and
+ * By default, the adapter uses a model called `Users`, and lookup fields called `'username'` and
* `'password'`. These can be customized by setting the `'model'` and `'fields'` configuration keys,
- * respectively. The `'model'` key accepts either a model name (i.e. `Customer`), or a
- * fully-namespaced path to a model class (i.e. `\app\models\Customer`). The `'fields'` setting
+ * respectively. The `'model'` key accepts either a model name (i.e. `Customers`), or a
+ * fully-namespaced path to a model class (i.e. `app\models\Customers`). The `'fields'` setting
* accepts an array of field names to use when looking up a user. An example configuration,
* including a custom model class and lookup fields might look like the following:
* {{{
* Auth::config(array(
* 'customer' => array(
- * 'model' => 'Customer',
+ * 'adapter' => 'Form',
+ * 'model' => 'Customers',
* 'fields' => array('email', 'password')
* )
* ));
@@ -42,7 +43,8 @@ use \lithium\core\Libraries;
* {{{
* Auth::config(array(
* 'customer' => array(
- * 'model' => 'Customer',
+ * 'adapter' => 'Form',
+ * 'model' => 'Customers',
* 'fields' => array('username' => 'login.username', 'password' => 'login.password'),
* 'scope' => array('active' => true)
* )
@@ -74,7 +76,7 @@ class Form extends \lithium\core\Object {
/**
* The name of the model class to query against. This can either be a model name (i.e.
- * `'User'`), or a fully-namespaced class reference (i.e. `'app\models\User'`). When
+ * `'Users'`), or a fully-namespaced class reference (i.e. `'app\models\Users'`). When
* authenticating users, the magic `first()` method is invoked against the model to return the
* first record found when combining the conditions in the `$_scope` property with the
* authentication data yielded from the `Request` object in `Form::check()`. (Note that the
@@ -103,8 +105,8 @@ class Form extends \lithium\core\Object {
protected $_scope = array();
/**
- * Callback filters to apply to request data before using it the authentication query. Each key
- * in the array must match a request field specified in the `$_fields` property, and each
+ * Callback filters to apply to request data before using it in the authentication query. Each
+ * key in the array must match a request field specified in the `$_fields` property, and each
* value must either be a reference to a function or method name, or a closure. For example, to
* automatically hash passwords, the `Form` adapter provides the following default
* configuration, i.e.: `array('password' => array('\lithium\util\String', 'hash'))`.
@@ -114,6 +116,7 @@ class Form extends \lithium\core\Object {
* {{{
* Auth::config(array(
* 'members' => array(
+ * 'adapter' => 'Form',
* 'model' => 'Member',
* 'fields' => array('email', 'password'),
* 'filters' => array(function($data) {
@@ -128,7 +131,6 @@ class Form extends \lithium\core\Object {
* ));
* }}}
*
- *
* @see lithium\security\auth\adapter\Form::$_fields
* @var array
*/
@@ -139,10 +141,10 @@ class Form extends \lithium\core\Object {
* which model method to call, and this method will receive the authentication query. In return,
* the `Form` adapter expects a `Record` object which implements the `data()` method. See the
* constructor for more information on setting this property. Defaults to `'first'`, which
- * calls, for example, `User::first()`.
+ * calls, for example, `Users::first()`.
*
* @see lithium\security\auth\adapter\Form::__construct()
- * @see lithium\data\model\Record::data()
+ * @see lithium\data\entity\Record::data()
* @var string
*/
protected $_query = '';
@@ -174,11 +176,11 @@ class Form extends \lithium\core\Object {
*/
public function __construct(array $config = array()) {
$defaults = array(
- 'model' => 'User', 'query' => 'first', 'filters' => array(), 'fields' => array(
+ 'model' => 'Users', 'query' => 'first', 'filters' => array(), 'fields' => array(
'username', 'password'
)
);
- parent::__construct((array) $config + $defaults);
+ parent::__construct($config + $defaults);
}
/**
@@ -196,7 +198,7 @@ class Form extends \lithium\core\Object {
public function check($credentials, array $options = array()) {
$model = $this->_model;
$query = $this->_query;
- $conditions = $this->_scope + $this->_filters($credentials->data);
+ $conditions = $this->_scope + $this->_filters(array_map('strval', $credentials->data));
$user = $model::$query(compact('conditions'));
return $user ? $user->data() : false;
}
@@ -232,15 +234,22 @@ class Form extends \lithium\core\Object {
$this->_model = Libraries::locate('models', $this->_model);
}
+ /**
+ * Calls each registered callback, by field name.
+ *
+ * @param string $data Keyed form data.
+ * @return mixed Callback result.
+ */
protected function _filters($data) {
$result = array();
foreach ($this->_fields as $key => $field) {
$result[$field] = isset($data[$key]) ? $data[$key] : null;
- if (isset($this->_filters[$key])) {
- $result[$field] = call_user_func($this->_filters[$key], $result[$field]);
+ if (!isset($this->_filters[$key])) {
+ continue;
}
+ $result[$field] = call_user_func($this->_filters[$key], $result[$field]);
}
return isset($this->_filters[0]) ? call_user_func($this->_filters[0], $result) : $result;
}
diff --git a/libraries/lithium/security/auth/adapter/Http.php b/libraries/lithium/security/auth/adapter/Http.php
new file mode 100644
index 0000000..cead8d6
--- /dev/null
+++ b/libraries/lithium/security/auth/adapter/Http.php
@@ -0,0 +1,155 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\security\auth\adapter;
+
+use lithium\core\Libraries;
+
+/**
+ * The `Http` adapter provides basic and digest authentication based on the HTTP protocol.
+ * By default, the adapter uses Http Digest based authentication.
+ * {{{
+ * Auth::config(array('name' => array('adapter' => 'Http', 'users' => array('gwoo' => 'li3'))))
+ * }}}
+ *
+ * To use Basic authentication, set the `method` to basic.
+ * {{{
+ * Auth::config(array('name' => array(
+ * 'adapter' => 'Http', 'users' => array('gwoo' => 'li3'),
+ * 'method' => 'basic'
+ * )))
+ * }}}
+ *
+ * @link http://tools.ietf.org/html/rfc2068#section-14.8
+ * @see `\lithium\action\Request`
+ */
+class Http extends \lithium\core\Object {
+
+ /**
+ * Setup default configuration options.
+ *
+ * @param array $config
+ * - `method`: default: `digest` options: `basic|digest`
+ * - `realm`: default: `Protected by Lithium`
+ * - `users`: the users to permit. key => value pair of username => password
+ */
+ public function __construct(array $config = array()) {
+ $defaults = array(
+ 'method' => 'digest', 'realm' => basename(LITHIUM_APP_PATH), 'users' => array()
+ );
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Called by the `Auth` class to run an authentication check against the HTTP data using the
+ * credientials in a data container (a `Request` object), and returns an array of user
+ * information on success, or `false` on failure.
+ *
+ * @param object $request A env container which wraps the authentication credentials used
+ * by HTTP (usually a `Request` object). See the documentation for this
+ * class for further details.
+ * @param array $options Additional configuration options. Not currently implemented in this
+ * adapter.
+ * @return array Returns an array containing user information on success, or `false` on failure.
+ */
+ public function check($request, array $options = array()) {
+ $method = "_{$this->_config['method']}";
+ return $this->{$method}($request);
+ }
+
+ /**
+ * A pass-through method called by `Auth`. Returns the value of `$data`, which is written to
+ * a user's session. When implementing a custom adapter, this method may be used to modify or
+ * reject data before it is written to the session.
+ *
+ * @param array $data User data to be written to the session.
+ * @param array $options Adapter-specific options. Not implemented in the `Form` adapter.
+ * @return array Returns the value of `$data`.
+ */
+ public function set($data, array $options = array()) {
+ return $data;
+ }
+
+ /**
+ * Called by `Auth` when a user session is terminated. Not implemented in the `Form` adapter.
+ *
+ * @param array $options Adapter-specific options. Not implemented in the `Form` adapter.
+ * @return void
+ */
+ public function clear(array $options = array()) {
+ }
+
+ /**
+ * Handler for HTTP Basic Authentication
+ *
+ * @param string $request a `\lithium\action\Request` object
+ * @return void
+ */
+ protected function _basic($request) {
+ $users = $this->_config['users'];
+ $username = $request->env('PHP_AUTH_USER');
+ $password = $request->env('PHP_AUTH_PW');
+
+ if (!isset($users[$username]) || $users[$username] !== $password) {
+ $this->_writeHeader("WWW-Authenticate: Basic realm=\"{$this->_config['realm']}\"");
+ return;
+ }
+ return compact('username', 'password');
+ }
+
+ /**
+ * Handler for HTTP Digest Authentication
+ *
+ * @param string $request a `\lithium\action\Request` object
+ * @return void
+ */
+ protected function _digest($request) {
+ $realm = $this->_config['realm'];
+ $data = array(
+ 'username' => null, 'nonce' => null, 'nc' => null,
+ 'cnonce' => null, 'qop' => null, 'uri' => null,
+ 'response' => null
+ );
+
+ $result = array_map(function ($string) use (&$data) {
+ $parts = explode('=', trim($string), 2) + array('', '');
+ $data[$parts[0]] = trim($parts[1], '"');
+ }, explode(',', $request->env('PHP_AUTH_DIGEST')));
+
+ $users = $this->_config['users'];
+ $password = !empty($users[$data['username']]) ? $users[$data['username']] : null;
+
+ $user = md5("{$data['username']}:{$realm}:{$password}");
+ $nonce = "{$data['nonce']}:{$data['nc']}:{$data['cnonce']}:{$data['qop']}";
+ $req = md5($request->env('REQUEST_METHOD') . ':' . $data['uri']);
+ $hash = md5("{$user}:{$nonce}:{$req}");
+
+ if (!$data['username'] || $hash !== $data['response']) {
+ $nonce = uniqid();
+ $opaque = md5($realm);
+
+ $message = "WWW-Authenticate: Digest realm=\"{$realm}\" qop=\"auth\",";
+ $message .= "nonce=\"{$nonce}\",opaque=\"{$opaque}\"";
+ $this->_writeHeader($message);
+ return;
+ }
+ return array('username' => $data['username'], 'password' => $password);
+ }
+
+ /**
+ * Helper method for writing headers. Mainly used to override the output while testing.
+ *
+ * @param string $string the string the send as a header
+ * @return void
+ */
+ protected function _writeHeader($string) {
+ header($string, true);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/Cache.php b/libraries/lithium/storage/Cache.php
index 7758e7b..c92be3f 100644
--- a/libraries/lithium/storage/Cache.php
+++ b/libraries/lithium/storage/Cache.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -89,17 +89,19 @@ class Cache extends \lithium\core\Adaptable {
* @param mixed $key Key to uniquely identify the cache entry
* @param mixed $data Data to be cached
* @param string $expiry A strtotime() compatible cache time
- * @param mixed $conditions Conditions for the write operation to proceed
+ * @param mixed $options Options for the method, filters and strategies.
* @return boolean True on successful cache write, false otherwise
* @filter This method may be filtered.
*/
- public static function write($name, $key, $data, $expiry = null, $conditions = null) {
+ public static function write($name, $key, $data, $expiry = null, array $options = array()) {
+ $options += array('conditions' => null, 'strategies' => true);
$settings = static::config();
if (!isset($settings[$name])) {
return false;
}
+ $conditions = $options['conditions'];
if (is_callable($conditions) && !$conditions()) {
return false;
}
@@ -109,7 +111,12 @@ class Cache extends \lithium\core\Adaptable {
$expiry = $data;
$data = null;
}
- $data = static::applyStrategies(__FUNCTION__, $name, $data);
+
+ if ($options['strategies']) {
+ $options = array('key' => $key, 'class' => __CLASS__);
+ $data = static::applyStrategies(__FUNCTION__, $name, $data, $options);
+ }
+
$method = static::adapter($name)->write($key, $data, $expiry);
$params = compact('key', 'data', 'expiry');
return static::_filter(__FUNCTION__, $params, $method, $settings[$name]['filters']);
@@ -121,17 +128,19 @@ class Cache extends \lithium\core\Adaptable {
*
* @param string $name Configuration to be used for reading
* @param mixed $key Key to be retrieved
- * @param mixed $conditions Conditions for the read operation to proceed
+ * @param mixed $options Options for the method and strategies.
* @return mixed Read results on successful cache read, null otherwise
* @filter This method may be filtered.
*/
- public static function read($name, $key, $conditions = null) {
+ public static function read($name, $key, array $options = array()) {
+ $options += array('conditions' => null, 'strategies' => true, 'write' => null);
$settings = static::config();
if (!isset($settings[$name])) {
return false;
}
+ $conditions = $options['conditions'];
if (is_callable($conditions) && !$conditions()) {
return false;
}
@@ -139,9 +148,20 @@ class Cache extends \lithium\core\Adaptable {
$method = static::adapter($name)->read($key);
$params = compact('key');
$filters = $settings[$name]['filters'];
-
$result = static::_filter(__FUNCTION__, $params, $method, $filters);
- return static::applyStrategies(__FUNCTION__, $name, $result, 'LIFO');
+
+ if ($result === null && $options['write']) {
+ $write = (is_callable($options['write'])) ? $options['write']() : $options['write'];
+ list($expiry, $value) = each($write);
+
+ return static::write($name, $key, $value, $expiry);
+ }
+
+ if ($options['strategies']) {
+ $options = array('key' => $key, 'mode' => 'LIFO', 'class' => __CLASS__);
+ $result = static::applyStrategies(__FUNCTION__, $name, $result, $options);
+ }
+ return $result;
}
/**
@@ -149,27 +169,32 @@ class Cache extends \lithium\core\Adaptable {
*
* @param string $name The cache configuration to delete from
* @param mixed $key Key to be deleted
- * @param mixed $conditions Conditions for the delete operation to proceed
+ * @param mixed $options Options for the method and strategies.
* @return boolean True on successful deletion, false otherwise
* @filter This method may be filtered.
*/
- public static function delete($name, $key, $conditions = null) {
+ public static function delete($name, $key, array $options = array()) {
+ $options += array('conditions' => null, 'strategies' => true);
$settings = static::config();
if (!isset($settings[$name])) {
return false;
}
+ $conditions = $options['conditions'];
if (is_callable($conditions) && !$conditions()) {
return false;
}
$key = static::key($key);
$method = static::adapter($name)->delete($key);
- $params = compact('key');
$filters = $settings[$name]['filters'];
- return static::_filter(__FUNCTION__, $params, $method, $filters);
+ if ($options['strategies']) {
+ $options += array('key' => $key, 'class' => __CLASS__);
+ $key = static::applyStrategies(__FUNCTION__, $name, $key, $options);
+ }
+ return static::_filter(__FUNCTION__, compact('key'), $method, $filters);
}
/**
@@ -179,16 +204,18 @@ class Cache extends \lithium\core\Adaptable {
* @param string $name
* @param string $key Key of numeric cache item to increment
* @param integer $offset Offset to increment - defaults to 1.
- * @param mixed $conditions
+ * @param mixed $options Options for this method.
* @return integer|boolean Item's new value on successful increment, false otherwise
* @filter This method may be filtered.
*/
- public static function increment($name, $key, $offset = 1, $conditions = null) {
+ public static function increment($name, $key, $offset = 1, array $options = array()) {
+ $options += array('conditions' => null);
$settings = static::config();
if (!isset($settings[$name])) {
return false;
}
+ $conditions = $options['conditions'];
if (is_callable($conditions) && !$conditions()) {
return false;
@@ -209,16 +236,18 @@ class Cache extends \lithium\core\Adaptable {
* @param string $name
* @param string $key Key of numeric cache item to dercrement
* @param integer $offset Offset to decrement - defaults to 1.
- * @param mixed $conditions
+ * @param mixed $options Options for this method.
* @return integer|boolean Item's new value on successful decrement, false otherwise
* @filter This method may be filtered.
*/
- public static function decrement($name, $key, $offset = 1, $conditions = null) {
+ public static function decrement($name, $key, $offset = 1, array $options = array()) {
+ $options += array('conditions' => null);
$settings = static::config();
if (!isset($settings[$name])) {
return false;
}
+ $conditions = $options['conditions'];
if (is_callable($conditions) && !$conditions()) {
return false;
diff --git a/libraries/lithium/storage/Session.php b/libraries/lithium/storage/Session.php
index bc825a4..e320463 100644
--- a/libraries/lithium/storage/Session.php
+++ b/libraries/lithium/storage/Session.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage;
-use \lithium\core\Libraries;
-use \lithium\util\Collection;
+use lithium\core\Libraries;
+use lithium\util\Collection;
/**
* The `Session` static class provides a consistent interface to configure and utilize the
@@ -38,7 +38,7 @@ class Session extends \lithium\core\Adaptable {
*
* @var object Collection of cache configurations
*/
- protected static $_configurations = null;
+ protected static $_configurations = array();
/**
* Libraries::locate() compatible path to adapters for this class.
@@ -48,6 +48,13 @@ class Session extends \lithium\core\Adaptable {
protected static $_adapters = 'adapter.storage.session';
/**
+ * Libraries::locate() compatible path to strategies for this class.
+ *
+ * @var string Dot-delimited path.
+ */
+ protected static $_strategies = 'strategy.storage.session';
+
+ /**
* Returns key used to identify the session.
*
* @param mixed $name Named session configuration.
@@ -70,18 +77,6 @@ class Session extends \lithium\core\Adaptable {
}
/**
- * Checks the validity of a previously-started session by running several checks, including
- * comparing the session start time to the expiration time set in the configuration, and any
- * security settings.
- *
- * @param string $name Named session configuration.
- * @return boolean Returns true if the current session is active and valid.
- */
- public static function isValid($name = null) {
-
- }
-
- /**
* Reads a value from a persistent session store.
*
* @param string $key Key to be read
@@ -89,8 +84,8 @@ class Session extends \lithium\core\Adaptable {
* @return mixed Read result on successful session read, null otherwise.
* @filter This method may be filtered.
*/
- public static function read($key, array $options = array()) {
- $defaults = array('name' => null);
+ public static function read($key = null, array $options = array()) {
+ $defaults = array('name' => null, 'strategies' => true);
$options += $defaults;
$method = ($name = $options['name']) ? static::adapter($name)->read($key, $options) : null;
$settings = static::_config($name);
@@ -105,9 +100,14 @@ class Session extends \lithium\core\Adaptable {
return null;
}
}
- $filters = $settings['filters'];
- $result = static::_filter(__METHOD__, compact('key', 'options'), $method, $filters);
- return static::applyStrategies(__FUNCTION__, $name, $result, 'LIFO');
+ $filters = $settings['filters'] ?: array();
+ $result = static::_filter(__FUNCTION__, compact('key', 'options'), $method, $filters);
+
+ if ($options['strategies']) {
+ $options += array('key' => $key, 'mode' => 'LIFO', 'class' => __CLASS__);
+ return static::applyStrategies(__FUNCTION__, $name, $result, $options);
+ }
+ return $result;
}
/**
@@ -120,7 +120,7 @@ class Session extends \lithium\core\Adaptable {
* @filter This method may be filtered.
*/
public static function write($key, $value = null, array $options = array()) {
- $defaults = array('name' => null);
+ $defaults = array('name' => null, 'strategies' => true);
$options += $defaults;
if (is_resource($value) || !static::$_configurations) {
@@ -139,12 +139,16 @@ class Session extends \lithium\core\Adaptable {
}
$result = false;
$settings = static::_config($name);
- $data = static::applyStrategies(__FUNCTION__, $name, $value);
+
+ if ($options['strategies']) {
+ $options += array('key' => $key, 'class' => __CLASS__);
+ $value = static::applyStrategies(__FUNCTION__, $name, $value, $options);
+ }
+ $params = compact('key', 'value', 'options');
foreach ($methods as $name => $method) {
- $params = compact('key', 'value', 'options');
$filters = $settings['filters'];
- $result = $result || static::_filter(__METHOD__, $params, $method, $filters);
+ $result = $result || static::_filter(__FUNCTION__, $params, $method, $filters);
}
return $result;
}
@@ -159,7 +163,7 @@ class Session extends \lithium\core\Adaptable {
* @filter This method may be filtered.
*/
public static function delete($key, array $options = array()) {
- $defaults = array('name' => null);
+ $defaults = array('name' => null, 'strategies' => true);
$options += $defaults;
$methods = array();
@@ -167,19 +171,59 @@ class Session extends \lithium\core\Adaptable {
if ($name = $options['name']) {
$methods = array($name => static::adapter($name)->delete($key, $options));
} else {
- foreach (array_keys(static::$_configurations) as $name) {
+ foreach (static::$_configurations as $name => $config) {
if ($method = static::adapter($name)->delete($key, $options)) {
$methods[$name] = $method;
}
}
}
$result = false;
- $settings = static::_config($name);
+ $options += array('key' => $key, 'class' => __CLASS__);
+
+ if ($options['strategies']) {
+ $options += array('key' => $key, 'class' => __CLASS__);
+ $key = static::applyStrategies(__FUNCTION__, $name, $key, $options);
+ }
+ $params = compact('key', 'options');
+ foreach ($methods as $name => $method) {
+ $settings = static::_config($name);
+ $filters = $settings['filters'];
+ $result = $result || static::_filter(__FUNCTION__, $params, $method, $filters);
+ }
+ return $result;
+ }
+
+ /**
+ * Clears all keys from a single adapter (if a `'name'` options is specified) or all
+ * session adapters.
+ *
+ * @param array $options Optional parameters that this method accepts.
+ */
+ public static function clear(array $options = array()) {
+ $defaults = array('name' => null, 'strategies' => true);
+ $options += $defaults;
+ $methods = array();
+
+ if ($name = $options['name']) {
+ $methods = array($name => static::adapter($name)->clear($options));
+ } else {
+ foreach (static::$_configurations as $name => $config) {
+ if ($method = static::adapter($name)->clear($options)) {
+ $methods[$name] = $method;
+ }
+ }
+ }
+ $params = compact('options');
+ $result = false;
foreach ($methods as $name => $method) {
- $params = compact('key', 'options');
+ $settings = static::_config($name);
$filters = $settings['filters'];
- $result = $result || static::_filter(__METHOD__, $params, $method, $filters);
+ $result = $result || static::_filter(__FUNCTION__, $params, $method, $filters);
+ }
+ if ($options['strategies']) {
+ $options += array('mode' => 'LIFO', 'class' => __CLASS__);
+ return static::applyStrategies(__FUNCTION__, $name, $result, $options);
}
return $result;
}
@@ -194,18 +238,32 @@ class Session extends \lithium\core\Adaptable {
* @filter This method may be filtered.
*/
public static function check($key, array $options = array()) {
- $defaults = array('name' => null);
+ $defaults = array('name' => null, 'strategies' => true);
$options += $defaults;
+ $methods = array();
- if ($options['name']) {
- return static::adapter($options['name'])->check($key, $options);
- }
- foreach (array_keys(static::$_configurations) as $name) {
- if (static::adapter($name)->check($key, $options)) {
- return true;
+ if ($name = $options['name']) {
+ $methods = array($name => static::adapter($name)->check($key, $options));
+ } else {
+ foreach (static::$_configurations as $name => $config) {
+ if ($method = static::adapter($name)->check($key, $options)) {
+ $methods[$name] = $method;
+ }
}
}
- return false;
+ $params = compact('key', 'options');
+ $result = false;
+
+ foreach ($methods as $name => $method) {
+ $settings = static::_config($name);
+ $filters = $settings['filters'];
+ $result = $result || static::_filter(__FUNCTION__, $params, $method, $filters);
+ }
+ if ($options['strategies']) {
+ $options += array('key' => $key, 'mode' => 'LIFO', 'class' => __CLASS__);
+ return static::applyStrategies(__FUNCTION__, $name, $result, $options);
+ }
+ return $result;
}
/**
diff --git a/libraries/lithium/storage/cache/adapter/Apc.php b/libraries/lithium/storage/cache/adapter/Apc.php
index 7e2a3bc..37acf16 100644
--- a/libraries/lithium/storage/cache/adapter/Apc.php
+++ b/libraries/lithium/storage/cache/adapter/Apc.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -46,7 +46,10 @@ class Apc extends \lithium\core\Object {
* @return void
*/
public function __construct(array $config = array()) {
- $defaults = array('prefix' => '');
+ $defaults = array(
+ 'prefix' => '',
+ 'expiry' => '+1 hour',
+ );
parent::__construct($config + $defaults);
}
@@ -59,12 +62,15 @@ class Apc extends \lithium\core\Object {
*
* @param string|array $key The key to uniquely identify the cached item.
* @param mixed $data The value to be cached.
- * @param string $expiry A strtotime() compatible cache time.
+ * @param null|string $expiry A strtotime() compatible cache time. If no expiry time is set,
+ * then the default cache expiration time set with the cache configuration will be used.
* @return boolean True on successful write, false otherwise.
*/
- public function write($key, $data, $expiry) {
- return function($self, $params, $chain) {
- $cachetime = strtotime($params['expiry']);
+ public function write($key, $data, $expiry = null) {
+ $expiry = ($expiry) ?: $this->_config['expiry'];
+
+ return function($self, $params) use ($expiry) {
+ $cachetime = (is_int($expiry) ? $expiry : strtotime($expiry)) - time();
$key = $params['key'];
if (is_array($key)) {
@@ -85,7 +91,7 @@ class Apc extends \lithium\core\Object {
* @return mixed Cached value if successful, false otherwise.
*/
public function read($key) {
- return function($self, $params, $chain) {
+ return function($self, $params) {
return apc_fetch($params['key']);
};
}
@@ -101,7 +107,7 @@ class Apc extends \lithium\core\Object {
* @return mixed True on successful delete, false otherwise.
*/
public function delete($key) {
- return function($self, $params, $chain) {
+ return function($self, $params) {
return apc_delete($params['key']);
};
}
@@ -118,7 +124,7 @@ class Apc extends \lithium\core\Object {
* @return mixed Item's new value on successful decrement, false otherwise
*/
public function decrement($key, $offset = 1) {
- return function($self, $params, $chain) use ($offset) {
+ return function($self, $params) use ($offset) {
return apc_dec($params['key'], $offset);
};
}
@@ -135,7 +141,7 @@ class Apc extends \lithium\core\Object {
* @return mixed Item's new value on successful increment, false otherwise
*/
public function increment($key, $offset = 1) {
- return function($self, $params, $chain) use ($offset) {
+ return function($self, $params) use ($offset) {
return apc_inc($params['key'], $offset);
};
}
@@ -156,7 +162,10 @@ class Apc extends \lithium\core\Object {
* return boolean True if enabled, false otherwise
*/
public static function enabled() {
- return (extension_loaded('apc') && apc_cache_info('user'));
+ $loaded = extension_loaded('apc');
+ $isCli = (php_sapi_name() === 'cli');
+ $enabled = (!$isCli && ini_get('apc.enabled')) || ($isCli && ini_get('apc.enable_cli'));
+ return ($loaded && $enabled);
}
}
diff --git a/libraries/lithium/storage/cache/adapter/File.php b/libraries/lithium/storage/cache/adapter/File.php
index e6e29f5..50ff6e0 100644
--- a/libraries/lithium/storage/cache/adapter/File.php
+++ b/libraries/lithium/storage/cache/adapter/File.php
@@ -2,14 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage\cache\adapter;
-use \SplFileInfo;
-use \DirectoryIterator;
+use SplFileInfo;
+use RecursiveIteratorIterator;
+use RecursiveDirectoryIterator;
+use lithium\core\Libraries;
/**
* A minimal file-based cache.
@@ -27,7 +29,7 @@ use \DirectoryIterator;
* This adapter does *not* allow multi-key operations for any methods.
*
* The path that the cached files will be written to defaults to
- * `LITHIUM_APP_PATH/resources/tmp/cache`, but is user-configurable on cache configuration.
+ * `<app>/resources/tmp/cache`, but is user-configurable on cache configuration.
*
* Note that the cache expiration time is stored within the first few bytes
* of the cached data, and is transparently added and/or removed when values
@@ -40,14 +42,21 @@ class File extends \lithium\core\Object {
/**
* Class constructor.
*
- * @param array $config Configuration parameters for this cache adapter.
- * These settings are indexed by name and queryable
- * through `Cache::config('name')`.
- * @return void
* @see lithium\storage\Cache::config()
+ * @param array $config Configuration parameters for this cache adapter. These settings are
+ * indexed by name and queryable through `Cache::config('name')`.
+ * The defaults are:
+ * - 'path' : Path where cached entries live `LITHIUM_APP_PATH . '/resources/tmp/cache'`.
+ * - 'expiry' : Default expiry time used if none is explicitly set when calling
+ * `Cache::write()`.
+ * @return void
*/
public function __construct(array $config = array()) {
- $defaults = array('path' => LITHIUM_APP_PATH . '/resources/tmp/cache');
+ $defaults = array(
+ 'path' => Libraries::get(true, 'resources') . '/tmp/cache',
+ 'prefix' => '',
+ 'expiry' => '+1 hour',
+ );
parent::__construct($config + $defaults);
}
@@ -56,21 +65,20 @@ class File extends \lithium\core\Object {
*
* @param string $key The key to uniquely identify the cached item.
* @param mixed $data The value to be cached.
- * @param string $expiry A strtotime() compatible cache time.
+ * @param null|string $expiry A strtotime() compatible cache time. If no expiry time is set,
+ * then the default cache expiration time set with the cache configuration will be used.
* @return boolean True on successful write, false otherwise.
*/
- public function write($key, $data, $expiry) {
+ public function write($key, $data, $expiry = null) {
$path = $this->_config['path'];
+ $expiry = ($expiry) ?: $this->_config['expiry'];
- return function($self, $params, $chain) use (&$path) {
- extract($params);
+ return function($self, $params) use (&$path, $expiry) {
$expiry = strtotime($expiry);
- $data = "{:expiry:{$expiry}}\n{$data}";
- $path = "$path/$key";
-
+ $data = "{:expiry:{$expiry}}\n{$params['data']}";
+ $path = "{$path}/{$params['key']}";
return file_put_contents($path, $data);
};
-
}
/**
@@ -82,7 +90,7 @@ class File extends \lithium\core\Object {
public function read($key) {
$path = $this->_config['path'];
- return function($self, $params, $chain) use (&$path) {
+ return function($self, $params) use (&$path) {
extract($params);
$path = "$path/$key";
$file = new SplFileInfo($path);
@@ -100,9 +108,7 @@ class File extends \lithium\core\Object {
return false;
}
return preg_replace('/^\{\:expiry\:\d+\}\\n/', '', $data, 1);
-
};
-
}
/**
@@ -114,7 +120,7 @@ class File extends \lithium\core\Object {
public function delete($key) {
$path = $this->_config['path'];
- return function($self, $params, $chain) use (&$path) {
+ return function($self, $params) use (&$path) {
extract($params);
$path = "$path/$key";
$file = new SplFileInfo($path);
@@ -122,7 +128,6 @@ class File extends \lithium\core\Object {
if ($file->isFile() && $file->isReadable()) {
return unlink($path);
}
-
return false;
};
}
@@ -163,15 +168,15 @@ class File extends \lithium\core\Object {
* @return mixed True on successful clear, false otherwise.
*/
public function clear() {
- $directory = new DirectoryIterator($this->_config['path']);
+ $base = new RecursiveDirectoryIterator($this->_config['path']);
+ $iterator = new RecursiveIteratorIterator($base);
- foreach ($directory as $file) {
+ foreach ($iterator as $file) {
if ($file->isFile()) {
- unlink($file->getPathInfo());
+ unlink($file->getPathName());
}
}
return true;
-
}
/**
diff --git a/libraries/lithium/storage/cache/adapter/Memcache.php b/libraries/lithium/storage/cache/adapter/Memcache.php
index 4278e69..57a5410 100644
--- a/libraries/lithium/storage/cache/adapter/Memcache.php
+++ b/libraries/lithium/storage/cache/adapter/Memcache.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage\cache\adapter;
-use \lithium\util\Set;
+use lithium\util\Set;
/**
* A Memcache (libmemcached) cache adapter implementation.
@@ -58,16 +58,16 @@ class Memcache extends \lithium\core\Object {
* Instantiates the Memcached object, adds appropriate servers to the pool,
* and configures any optional settings passed.
*
+ * @see lithium\storage\Cache::config()
* @param array $config Configuration parameters for this cache adapter.
* These settings are indexed by name and queryable
* through `Cache::config('name')`.
- *
* @return void
- * @see lithium\storage\Cache::config()
*/
public function __construct(array $config = array()) {
$defaults = array(
'prefix' => '',
+ 'expiry' => '+1 hour',
'servers' => array(
array('127.0.0.1', 11211, 100)
)
@@ -91,14 +91,16 @@ class Memcache extends \lithium\core\Object {
*
* @param string|array $key The key to uniquely identify the cached item.
* @param mixed $value The value to be cached.
- * @param string $expiry A strtotime() compatible cache time.
+ * @param null|string $expiry A strtotime() compatible cache time. If no expiry time is set,
+ * then the default cache expiration time set with the cache configuration will be used.
* @return boolean True on successful write, false otherwise.
*/
- public function write($key, $value, $expiry) {
+ public function write($key, $value, $expiry = null) {
$connection =& static::$connection;
+ $expiry = ($expiry) ?: $this->_config['expiry'];
- return function($self, $params, $chain) use (&$connection) {
- $expires = strtotime($params['expiry']);
+ return function($self, $params) use (&$connection, $expiry) {
+ $expires = strtotime($expiry);
$key = $params['key'];
if (is_array($key)) {
@@ -122,7 +124,7 @@ class Memcache extends \lithium\core\Object {
public function read($key) {
$connection =& static::$connection;
- return function($self, $params, $chain) use (&$connection) {
+ return function($self, $params) use (&$connection) {
$key = $params['key'];
if (is_array($key)) {
@@ -141,7 +143,7 @@ class Memcache extends \lithium\core\Object {
public function delete($key) {
$connection =& static::$connection;
- return function($self, $params, $chain) use (&$connection) {
+ return function($self, $params) use (&$connection) {
return $connection->delete($params['key']);
};
}
@@ -161,7 +163,7 @@ class Memcache extends \lithium\core\Object {
public function decrement($key, $offset = 1) {
$connection =& static::$connection;
- return function($self, $params, $chain) use (&$connection, $offset) {
+ return function($self, $params) use (&$connection, $offset) {
return $connection->decrement($params['key'], $offset);
};
}
@@ -180,7 +182,7 @@ class Memcache extends \lithium\core\Object {
public function increment($key, $offset = 1) {
$connection =& static::$connection;
- return function($self, $params, $chain) use (&$connection, $offset) {
+ return function($self, $params) use (&$connection, $offset) {
return $connection->increment($params['key'], $offset);
};
}
diff --git a/libraries/lithium/storage/cache/adapter/Memory.php b/libraries/lithium/storage/cache/adapter/Memory.php
index fe06574..5d17379 100644
--- a/libraries/lithium/storage/cache/adapter/Memory.php
+++ b/libraries/lithium/storage/cache/adapter/Memory.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -56,7 +56,7 @@ class Memory extends \lithium\core\Object {
public function read($key) {
$cache =& $this->_cache;
- return function($self, $params, $chain) use (&$cache) {
+ return function($self, $params) use (&$cache) {
extract($params);
if (is_array($key)) {
@@ -87,7 +87,7 @@ class Memory extends \lithium\core\Object {
public function write($key, $data, $expiry) {
$cache =& $this->_cache;
- return function($self, $params, $chain) use (&$cache) {
+ return function($self, $params) use (&$cache) {
extract($params);
if (is_array($key)) {
@@ -109,7 +109,7 @@ class Memory extends \lithium\core\Object {
public function delete($key) {
$cache =& $this->_cache;
- return function($self, $params, $chain) use (&$cache) {
+ return function($self, $params) use (&$cache) {
extract($params);
if (isset($cache[$key])) {
unset($cache[$key]);
@@ -130,7 +130,7 @@ class Memory extends \lithium\core\Object {
public function decrement($key, $offset = 1) {
$cache =& $this->_cache;
- return function($self, $params, $chain) use (&$cache, $offset) {
+ return function($self, $params) use (&$cache, $offset) {
extract($params);
return $cache[$key] -= 1;
};
@@ -146,7 +146,7 @@ class Memory extends \lithium\core\Object {
public function increment($key, $offset = 1) {
$cache =& $this->_cache;
- return function($self, $params, $chain) use (&$cache, $offset) {
+ return function($self, $params) use (&$cache, $offset) {
extract($params);
return $cache[$key] += 1;
};
diff --git a/libraries/lithium/storage/cache/adapter/Redis.php b/libraries/lithium/storage/cache/adapter/Redis.php
index be654ed..47b6ac8 100644
--- a/libraries/lithium/storage/cache/adapter/Redis.php
+++ b/libraries/lithium/storage/cache/adapter/Redis.php
@@ -2,17 +2,19 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage\cache\adapter;
+use Redis as RedisCore;
+
/**
* A Redis (phpredis) cache adapter implementation.
*
* This adapter uses the `phpredis` PHP extension, which can be found here:
- * http://github.com/owlient/phpredis
+ * https://github.com/nicolasff/phpredis
*
* The Redis cache adapter is meant to be used through the `Cache` interface,
* which abstracts away key generation, adapter instantiation and filter
@@ -27,12 +29,12 @@ namespace lithium\storage\cache\adapter;
* Cache::config(array(
* 'cache-config-name' => array(
* 'adapter' => 'Redis',
- * 'server' => '127.0.0.1:6379'
+ * 'host' => '127.0.0.1:6379'
* )
* ));
* }}}
*
- * The 'server' key accepts a string argument in the format of ip:port where the Redis
+ * The 'host' key accepts a string argument in the format of ip:port where the Redis
* server can be found.
*
* This Redis adapter provides basic support for `write`, `read`, `delete`
@@ -41,7 +43,7 @@ namespace lithium\storage\cache\adapter;
*
* @see lithium\storage\Cache::key()
* @see lithium\storage\Cache::adapter()
- * @see http://github.com/owlient/phpredis
+ * @link https://github.com/nicolasff/phpredis GitHub: PhpRedis Extension
*
*/
class Redis extends \lithium\core\Object {
@@ -51,47 +53,63 @@ class Redis extends \lithium\core\Object {
*
* @var object Redis object
*/
- public static $connection = null;
+ public $connection;
/**
* Object constructor
*
- * Instantiates the Redis object and connects it to the configured server.
+ * Instantiates the `Redis` object and connects it to the configured server.
*
+ * @todo Implement configurable & optional authentication
+ * @see lithium\storage\Cache::config()
+ * @see lithium\storage\cache\adapter\Redis::_ttl()
* @param array $config Configuration parameters for this cache adapter.
- * These settings are indexed by name and queryable through `Cache::config('name')`.
- *
+ * These settings are indexed by name and queryable through `Cache::config('name')`. The
+ * available settings for this adapter are as follows:
+ * - `'host'` _string_: A string in the form of `'host:port'` indicating the Redis server
+ * to connect to. Defaults to `'127.0.0.1:6379'`.
+ * - `'expiry'` _mixed_: Default expiration for cache values written through this
+ * adapter. Defaults to `'+1 hour'`. For acceptable values, see the `$expiry` parameter
+ * of `Redis::_ttl()`.
+ * - `'persistent'` _boolean_: Indicates whether the adapter should use a persistent
+ * connection when attempting to connect to the Redis server. If `true`, it will
+ * attempt to reuse an existing connection when connecting, and the connection will
+ * not close when the request is terminated. Defaults to `false`.
* @return void
- * @see lithium\storage\Cache::config()
- * @todo Implement configurable & optional authentication
*/
public function __construct(array $config = array()) {
$defaults = array(
- 'prefix' => '',
- 'server' => '127.0.0.1:6379'
+ 'host' => '127.0.0.1:6379',
+ 'expiry' => '+1 hour',
+ 'persistent' => false,
);
+ parent::__construct($config + $defaults);
+ }
- if (is_null(static::$connection)) {
- static::$connection = new \Redis();
+ /**
+ * Initialize the Redis connection object and connect to the Redis server.
+ *
+ * @return void
+ */
+ protected function _init() {
+ if (!$this->connection) {
+ $this->connection = new RedisCore();
}
-
- $config += $defaults;
- parent::__construct($config);
-
- list($IP, $port) = explode(':', $this->_config['server']);
- static::$connection->connect($IP, $port);
+ list($ip, $port) = explode(':', $this->_config['host']);
+ $method = $this->_config['persistent'] ? 'pconnect' : 'connect';
+ $this->connection->{$method}($ip, $port);
}
/**
* Sets expiration time for cache keys
*
* @param string $key The key to uniquely identify the cached item
- * @param string $expiry A strtotime() compatible cache time
- * @return boolean True if expiry could be set for the given key, false otherwise
+ * @param mixed $expiry A `strtotime()`-compatible string indicating when the cached item
+ * should expire, or a Unix timestamp.
+ * @return boolean Returns `true` if expiry could be set for the given key, `false` otherwise.
*/
protected function _ttl($key, $expiry) {
- $expires = strtotime($expiry) - time();
- return static::$connection->setTimeout($key, $expires);
+ return $this->connection->expireAt($key, is_int($expiry) ? $expiry : strtotime($expiry));
}
/**
@@ -99,15 +117,35 @@ class Redis extends \lithium\core\Object {
*
* @param string $key The key to uniquely identify the cached item
* @param mixed $value The value to be cached
- * @param string $expiry A strtotime() compatible cache time
+ * @param null|string $expiry A strtotime() compatible cache time. If no expiry time is set,
+ * then the default cache expiration time set with the cache configuration will be used.
* @return boolean True on successful write, false otherwise
*/
- public function write($key, $value, $expiry) {
- $connection =& static::$connection;
-
- return function($self, $params, $chain) use (&$connection) {
- if($connection->set($params['key'], $params['data'])){
- return $self->invokeMethod('_ttl', array($params['key'], $params['expiry']));
+ public function write($key, $value = null, $expiry = null) {
+ $connection =& $this->connection;
+ $expiry = ($expiry) ?: $this->_config['expiry'];
+ $_self =& $this;
+
+ return function($self, $params) use (&$_self, &$connection, $expiry) {
+ if (is_array($params['key'])) {
+ $expiry = $params['data'];
+
+ if ($connection->mset($params['key'])) {
+ $ttl = array();
+
+ if ($expiry) {
+ foreach ($params['key'] as $k => $v) {
+ $ttl[$k] = $_self->invokeMethod('_ttl', array($k, $expiry));
+ }
+ }
+ return $ttl;
+ }
+ }
+ if ($result = $connection->set($params['key'], $params['data'])){
+ if ($expiry) {
+ return $_self->invokeMethod('_ttl', array($params['key'], $expiry));
+ }
+ return $result;
}
};
}
@@ -119,10 +157,15 @@ class Redis extends \lithium\core\Object {
* @return mixed Cached value if successful, false otherwise
*/
public function read($key) {
- $connection =& static::$connection;
+ $connection =& $this->connection;
- return function($self, $params, $chain) use (&$connection) {
- return $connection->get($params['key']);
+ return function($self, $params) use (&$connection) {
+ $key = $params['key'];
+
+ if (is_array($key)) {
+ return $connection->getMultiple($key);
+ }
+ return $connection->get($key);
};
}
@@ -133,9 +176,9 @@ class Redis extends \lithium\core\Object {
* @return mixed True on successful delete, false otherwise
*/
public function delete($key) {
- $connection =& static::$connection;
+ $connection =& $this->connection;
- return function($self, $params, $chain) use (&$connection) {
+ return function($self, $params) use (&$connection) {
return (boolean) $connection->delete($params['key']);
};
}
@@ -143,14 +186,18 @@ class Redis extends \lithium\core\Object {
/**
* Performs an atomic decrement operation on specified numeric cache item.
*
+ * Note that if the value of the specified key is *not* an integer, the decrement
+ * operation will have no effect whatsoever. Redis chooses to not typecast values
+ * to integers when performing an atomic decrement operation.
+ *
* @param string $key Key of numeric cache item to decrement
* @param integer $offset Offset to decrement - defaults to 1.
* @return mixed Item's new value on successful decrement, false otherwise
*/
public function decrement($key, $offset = 1) {
- $connection =& static::$connection;
+ $connection =& $this->connection;
- return function($self, $params, $chain) use (&$connection, $offset) {
+ return function($self, $params) use (&$connection, $offset) {
return $connection->decr($params['key'], $offset);
};
}
@@ -158,14 +205,18 @@ class Redis extends \lithium\core\Object {
/**
* Performs an atomic increment operation on specified numeric cache item.
*
+ * Note that if the value of the specified key is *not* an integer, the increment
+ * operation will have no effect whatsoever. Redis chooses to not typecast values
+ * to integers when performing an atomic increment operation.
+ *
* @param string $key Key of numeric cache item to increment
* @param integer $offset Offset to increment - defaults to 1.
* @return mixed Item's new value on successful increment, false otherwise
*/
public function increment($key, $offset = 1) {
- $connection =& static::$connection;
+ $connection =& $this->connection;
- return function($self, $params, $chain) use (&$connection, $offset) {
+ return function($self, $params) use (&$connection, $offset) {
return $connection->incr($params['key'], $offset);
};
}
@@ -176,7 +227,7 @@ class Redis extends \lithium\core\Object {
* @return mixed True on successful clear, false otherwise
*/
public function clear() {
- return static::$connection->flushdb();
+ return $this->connection->flushdb();
}
/**
diff --git a/libraries/lithium/storage/cache/adapter/XCache.php b/libraries/lithium/storage/cache/adapter/XCache.php
index bfb55f6..b64d036 100644
--- a/libraries/lithium/storage/cache/adapter/XCache.php
+++ b/libraries/lithium/storage/cache/adapter/XCache.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -15,10 +15,12 @@ namespace lithium\storage\cache\adapter;
* which abstracts away key generation, adapter instantiation and filter
* implementation.
*
- * A simple configuration of this adapter can be accomplished in `app/config/bootstrap.php`
+ * A simple configuration of this adapter can be accomplished in `app/config/bootstrap/cache.php`
* as follows:
*
* {{{
+ * use lithium\storage\Cache;
+ *
* Cache::config(array(
* 'cache-config-name' => array(
* 'adapter' => 'XCache',
@@ -29,7 +31,7 @@ namespace lithium\storage\cache\adapter;
* }}}
*
* Note that the `username` and `password` configuration fields are only required if
- * you wish to use XCache::clear() - all other methods do not require XCache administrator
+ * you wish to use `XCache::clear()` - all other methods do not require XCache administrator
* credentials.
*
* This XCache adapter provides basic support for `write`, `read`, `delete`
@@ -50,7 +52,7 @@ class XCache extends \lithium\core\Object {
* @return void
*/
public function __construct(array $config = array()) {
- $defaults = array('prefix' => '');
+ $defaults = array('prefix' => '', 'expiry' => '+1 hour');
parent::__construct($config + $defaults);
}
@@ -59,12 +61,15 @@ class XCache extends \lithium\core\Object {
*
* @param string $key The key to uniquely identify the cached item
* @param mixed $data The value to be cached
- * @param string $expiry A strtotime() compatible cache time
+ * @param null|string $expiry A strtotime() compatible cache time. If no expiry time is set,
+ * then the default cache expiration time set with the cache configuration will be used.
* @return boolean True on successful write, false otherwise
*/
- public function write($key, $data, $expiry) {
- return function($self, $params, $chain) {
- return xcache_set($params['key'], $params['data'], strtotime($params['expiry']));
+ public function write($key, $data, $expiry = null) {
+ $expiry = ($expiry) ?: $this->_config['expiry'];
+
+ return function($self, $params) use ($expiry) {
+ return xcache_set($params['key'], $params['data'], strtotime($expiry) - time());
};
}
@@ -75,7 +80,7 @@ class XCache extends \lithium\core\Object {
* @return mixed Cached value if successful, false otherwise
*/
public function read($key) {
- return function($self, $params, $chain) {
+ return function($self, $params) {
return xcache_get($params['key']);
};
}
@@ -87,7 +92,7 @@ class XCache extends \lithium\core\Object {
* @return mixed True on successful delete, false otherwise
*/
public function delete($key) {
- return function($self, $params, $chain) {
+ return function($self, $params) {
return xcache_unset($params['key']);
};
}
@@ -104,7 +109,7 @@ class XCache extends \lithium\core\Object {
* @return mixed Item's new value on successful decrement, false otherwise
*/
public function decrement($key, $offset = 1) {
- return function($self, $params, $chain) use ($offset) {
+ return function($self, $params) use ($offset) {
return xcache_dec($params['key'], $offset);
};
}
@@ -120,7 +125,7 @@ class XCache extends \lithium\core\Object {
* @return mixed Item's new value on successful increment, false otherwise
*/
public function increment($key, $offset = 1) {
- return function($self, $params, $chain) use ($offset) {
+ return function($self, $params) use ($offset) {
extract($params);
return xcache_inc($params['key'], $offset);
};
diff --git a/libraries/lithium/storage/cache/strategy/Base64.php b/libraries/lithium/storage/cache/strategy/Base64.php
index 24eb606..73ba845 100644
--- a/libraries/lithium/storage/cache/strategy/Base64.php
+++ b/libraries/lithium/storage/cache/strategy/Base64.php
@@ -2,38 +2,40 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage\cache\strategy;
/**
- * A PHP base64 encoding strategy.
+ * A PHP base64-encoding strategy.
*/
class Base64 extends \lithium\core\Object {
/**
* Write strategy method.
- * Base64 encodes the passed data.
*
- * @see http://php.net/manual/en/function.base64-encode.php
+ * Base64-encodes the passed data.
+ *
+ * @link http://php.net/manual/en/function.base64-encode.php PHP Manual: base64_encode()
* @param mixed $data The data to be serialized.
* @return string Serialized data.
*/
- public static function write($data) {
+ public function write($data) {
return base64_encode($data);
}
/**
* Read strategy method.
+ *
* Unserializes the passed data.
*
- * @see http://php.net/manual/en/function.base64-decode.php
+ * @link http://php.net/manual/en/function.base64-decode.php PHP Manual: base64_decode()
* @param string $data Serialized data.
* @return mixed Result of unserialization.
*/
- public static function read($data) {
+ public function read($data) {
return base64_decode($data);
}
}
diff --git a/libraries/lithium/storage/cache/strategy/Json.php b/libraries/lithium/storage/cache/strategy/Json.php
index 71ed7fb..16497b4 100644
--- a/libraries/lithium/storage/cache/strategy/Json.php
+++ b/libraries/lithium/storage/cache/strategy/Json.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -15,26 +15,28 @@ class Json extends \lithium\core\Object {
/**
* Write strategy method.
- * Will json_encode to the passed data.
*
- * @see http://php.net/manual/en/function.json_encode.php
+ * Encodes the passed data from an array to JSON format.
+ *
+ * @link http://php.net/manual/en/function.json-encode.php PHP Manual: json_encode()
* @param mixed $data The data to be encoded.
* @return string The encoded data.
*/
- public static function write($data) {
+ public function write($data) {
return json_encode($data);
}
/**
* Read strategy method.
- * Applies json_decode to the passed data.
*
- * @see http://php.net/manual/en/function.json_decode.php
+ * Decodes JSON data and returns an array or object structure.
+ *
+ * @link http://php.net/manual/en/function.json-decode.php PHP Manual: json_decode()
* @param string $data Serialized data.
* @return mixed Result of unserialization.
*/
- public static function read($data) {
- return json_decode($data);
+ public function read($data) {
+ return json_decode($data, true);
}
}
diff --git a/libraries/lithium/storage/cache/strategy/Serializer.php b/libraries/lithium/storage/cache/strategy/Serializer.php
index 96c72b5..8b99693 100644
--- a/libraries/lithium/storage/cache/strategy/Serializer.php
+++ b/libraries/lithium/storage/cache/strategy/Serializer.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -15,25 +15,27 @@ class Serializer extends \lithium\core\Object {
/**
* Write strategy method.
+ *
* Serializes the passed data.
*
- * @see http://php.net/manual/en/function.serialize.php
+ * @link http://php.net/manual/en/function.serialize.php PHP Manual: serialize()
* @param mixed $data The data to be serialized.
* @return string Serialized data.
*/
- public static function write($data) {
+ public function write($data) {
return serialize($data);
}
/**
* Read strategy method.
+ *
* Unserializes the passed data.
*
- * @see http://php.net/manual/en/function.unserialize.php
+ * @link http://php.net/manual/en/function.unserialize.php PHP Manual: unserialize()
* @param string $data Serialized data.
* @return mixed Result of unserialization.
*/
- public static function read($data) {
+ public function read($data) {
return unserialize($data);
}
}
diff --git a/libraries/lithium/storage/session/adapter/Cache.php b/libraries/lithium/storage/session/adapter/Cache.php
deleted file mode 100644
index c40d9dd..0000000
--- a/libraries/lithium/storage/session/adapter/Cache.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\storage\session\adapter;
-
-/**
- * The `Cache` adapter is a simple session adapter which allows session data to be written to a
- * cache configuration.
- *
- * In order to use this adapter, you must first create a named cache configuration to connect the
- * adapter to. For example:
- *
- * {{{
- * use \lithium\storage\Cache;
- *
- * Cache::config(array(
- * 'local' => array('adapter' => 'Apc'),
- * 'distributed' => array(
- * 'adapter' => 'Memcached',
- * 'servers' => array('127.0.0.1', 11211),
- * )
- * ));}}}
- *
- * Then, you can configure your session storage:
- *
- * {{{
- * use \lithium\storage\Session;
- *
- * Session::config(array(
- * 'default' => array('adapter' => 'Cache', 'config' => 'distributed')
- * ));
- * }}}
- *
- * This will cause your users' session data to be written to a Memcache server. See the constructor
- * for additional information on the available configuration settings for this adapter.
- */
-class Cache extends \lithium\core\Object {
-
- protected $_classes = array(
- 'cache' => '\lithium\storage\Cache'
- );
-
- /**
- * Sets up the adapter with the configuration assigned by the `Session` class.
- *
- * @param array $config Available configuration options for this adapter:
- * - `'config'` _string_: The name of the cache configuration (created in the
- * `Cache` class) with which this adapter should interact.
- * - `'expiry'` _string_: A `strtotime()`-compatible string indicating when the
- * session store should expire cached session data.
- */
- public function __construct(array $config = array()) {
- $defaults = array(
- 'config' => null,
- 'expiry' => '+999 days'
- );
- parent::__construct($config + $defaults);
- }
-
- /**
- * Uses PHP's default session handling to generate a unique session ID.
- *
- * @return string Returns the session ID for the current request, or `null` if the session is
- * invalid or if a key could not be generated.
- */
- public function key() {
- return ($id = session_id()) == '' ? null : $id;
- }
-
- public function write($key, $value = null, array $options = array()) {
- $config = $this->_config;
- $classes = $this->_classes;
-
- return function($self, $params, $chain) use ($config, $classes) {
- return $classes['cache']::write(
- $config['config'], $params['key'], $params['value']
- );
- };
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/session/adapter/Cookie.php b/libraries/lithium/storage/session/adapter/Cookie.php
index 89b1ce3..170ff07 100644
--- a/libraries/lithium/storage/session/adapter/Cookie.php
+++ b/libraries/lithium/storage/session/adapter/Cookie.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage\session\adapter;
-use \lithium\util\Set;
+use lithium\util\Set;
+use lithium\util\Inflector;
/**
* A minimal adapter to interface with HTTP cookies.
@@ -28,7 +29,7 @@ class Cookie extends \lithium\core\Object {
* strtotime-compatible string instead of an epochal timestamp.
*/
protected $_defaults = array(
- 'name' => 'app', 'expire' => '+2 days', 'path' => '/',
+ 'expire' => '+2 days', 'path' => '/', 'name' => null,
'domain' => '', 'secure' => false, 'httponly' => false
);
@@ -45,6 +46,19 @@ class Cookie extends \lithium\core\Object {
}
/**
+ * Initialization of the cookie adapter.
+ *
+ * @return void
+ */
+ protected function _init() {
+ parent::_init();
+
+ if (!$this->_config['name']) {
+ $this->_config['name'] = Inflector::slug(basename(LITHIUM_APP_PATH)) . 'cookie';
+ }
+ }
+
+ /**
* Obtain the top-level cookie key.
*
* @return string The configured cookie 'name' parameter
@@ -79,8 +93,10 @@ class Cookie extends \lithium\core\Object {
* @return boolean True if the key exists, false otherwise.
*/
public function check($key) {
- return function($self, $params, $chain) {
- return (isset($_COOKIE[$params['key']]));
+ $config = $this->_config;
+
+ return function($self, $params) use (&$config) {
+ return (isset($_COOKIE[$config['name']][$params['key']]));
};
}
@@ -89,15 +105,34 @@ class Cookie extends \lithium\core\Object {
*
* @param null|string $key Key of the entry to be read. If $key is null, returns
* all cookie key/value pairs that have been set.
+ * @param array $options Options array. Not used in this adapter.
* @return mixed Data in the session if successful, null otherwise.
*/
- public function read($key = null) {
- return function($self, $params, $chain) {
+ public function read($key = null, array $options = array()) {
+ $config = $this->_config;
+
+ return function($self, $params) use (&$config) {
$key = $params['key'];
if (!$key) {
- return $_COOKIE;
+ if (isset($_COOKIE[$config['name']])) {
+ return $_COOKIE[$config['name']];
+ }
+ return array();
+ }
+ if (strpos($key, '.') !== false) {
+ $key = explode('.', $key);
+ $result = (isset($_COOKIE[$config['name']])) ? $_COOKIE[$config['name']] : array();
+
+ foreach ($key as $k) {
+ if (isset($result[$k])) {
+ $result = $result[$k];
+ }
+ }
+ return ($result !== array()) ? $result : null;
+ }
+ if (isset($_COOKIE[$config['name']][$key])) {
+ return $_COOKIE[$config['name']][$key];
}
- return (isset($_COOKIE[$key])) ? $_COOKIE[$key] : null;
};
}
@@ -110,32 +145,34 @@ class Cookie extends \lithium\core\Object {
* @return boolean True on successful write, false otherwise.
*/
public function write($key, $value = null, array $options = array()) {
- $expire = !isset($options['expire']) && empty($this->_config['expire']);
+ $expire = (!isset($options['expire']) && empty($this->_config['expire']));
+ $config = $this->_config;
+ $cookieClass = __CLASS__;
- if ($expire && $key != $this->_config['name']) {
+ if ($expire && $key != $config['name']) {
return null;
}
- $config = $options + $this->_config;
$expires = (isset($options['expire'])) ? $options['expire'] : $config['expire'];
- return function($self, $params, $chain) use (&$config, &$expires) {
+ return function($self, $params) use (&$config, &$expires, $cookieClass) {
$key = $params['key'];
$value = $params['value'];
- $key = is_array($key) ? Set::flatten($key) : array($key => $value);
+ $key = array($key => $value);
+ if (is_array($value)) {
+ $key = Set::flatten($key);
+ }
foreach ($key as $name => $val) {
- $name = explode('.', $name);
- $name = $config['name'] ? array_merge(array($config['name']), $name) : $name;
-
- if (count($name) == 1) {
- $name = current($name);
- } else {
- $name = (array_shift($name) . '[' . join('][', $name) . ']');
- }
- setcookie($name, $val, strtotime($expires), $config['path'],
+ $name = $cookieClass::keyFormat($name, $config);
+ $result = setcookie($name, $val, strtotime($expires), $config['path'],
$config['domain'], $config['secure'], $config['httponly']
);
+
+ if (!$result) {
+ throw new RuntimeException("There was an error setting {$name} cookie.");
+ }
}
+ return true;
};
}
@@ -147,27 +184,76 @@ class Cookie extends \lithium\core\Object {
* @return boolean True on successful delete, false otherwise.
*/
public function delete($key, array $options = array()) {
- $config = $options + $this->_config;
+ $config = $this->_config;
+ $cookieClass = get_called_class();
- return function($self, $params, $chain) use (&$config) {
+ return function($self, $params) use (&$config, $cookieClass) {
$key = $params['key'];
- $key = is_array($key) ? Set::flatten($key) : array($key);
+ $path = '/' . str_replace('.', '/', $config['name'] . '.' . $key) . '/.';
+ $cookies = current(Set::extract($_COOKIE, $path));
+ if (is_array($cookies)) {
+ $cookies = array_keys(Set::flatten($cookies));
+ foreach ($cookies as &$name) {
+ $name = $key . '.' . $name;
+ }
+ } else {
+ $cookies = array($key);
+ }
+ foreach ($cookies as &$name) {
+ $name = $cookieClass::keyFormat($name, $config);
+ $result = setcookie($name, "", 1, $config['path'],
+ $config['domain'], $config['secure'], $config['httponly']
+ );
+ if (!$result) {
+ throw new RuntimeException("There was an error deleting {$name} cookie.");
+ }
+ }
+ return true;
+ };
+ }
- foreach ($key as $name) {
- $name = explode('.', $name);
- $name = $config['name'] ? array_merge(array($config['name']), $name) : $name;
+ /**
+ * Clears all cookies.
+ *
+ * @param array $options Options array. Not used fro this adapter method.
+ * @return boolean True on successful clear, false otherwise.
+ */
+ public function clear(array $options = array()) {
+ $options += array('destroySession' => true);
+ $config = $this->_config;
+ $cookieClass = get_called_class();
- if (count($name) == 1) {
- $name = current($name);
- } else {
- $name = (array_shift($name) . '[' . join('][', $name) . ']');
- }
- setcookie($name, "", time() - 1, $config['path'],
+ return function($self, $params) use (&$config, $options, $cookieClass) {
+ if ($options['destroySession'] && session_id()) {
+ session_destroy();
+ }
+ if (!isset($_COOKIE[$config['name']])) {
+ return true;
+ }
+ $cookies = array_keys(Set::flatten($_COOKIE[$config['name']]));
+ foreach ($cookies as $name) {
+ $name = $cookieClass::keyFormat($name, $config);
+ $result = setcookie($name, "", 1, $config['path'],
$config['domain'], $config['secure'], $config['httponly']
);
+ if (!$result) {
+ throw new RuntimeException("There was an error clearing {$cookie} cookie.");
+ }
}
+ unset($_COOKIE[$config['name']]);
+ return true;
};
}
+
+ /**
+ * Formats the given `$name` argument for use in the cookie adapter.
+ *
+ * @param string $name The key to be formatted, e.g. `foo.bar.baz`.
+ * @return string The formatted key.
+ */
+ public static function keyFormat($name, $config) {
+ return $config['name'] . '[' . str_replace('.', '][', $name) . ']';
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/session/adapter/Memory.php b/libraries/lithium/storage/session/adapter/Memory.php
index ac8f693..0478a08 100644
--- a/libraries/lithium/storage/session/adapter/Memory.php
+++ b/libraries/lithium/storage/session/adapter/Memory.php
@@ -2,40 +2,72 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage\session\adapter;
-use \lithium\util\String;
+use lithium\util\String;
/**
* Simple memory session storage engine. Used for testing.
*/
class Memory extends \lithium\core\Object {
+ /**
+ * Holds the array that corresponds to session keys & values.
+ *
+ * @var array "Session" data.
+ */
public $_session = array();
- public function key() {
- $context = function ($value) use (&$config) {
- return (isset($_SERVER['SERVER_ADDR'])) ? $_SERVER['SERVER_ADDR'] : '127.0.0.1';
- };
- return String::uuid($context);
+ /**
+ * Obtain the session key.
+ *
+ * For this adapter, it is a UUID.
+ *
+ * @return string UUID.
+ */
+ public static function key() {
+ return String::uuid();
}
+ /**
+ * The memory adapter session is always "on".
+ *
+ * @return boolean True.
+ */
public function isStarted() {
return true;
}
+ /**
+ * Checks if a value has been set in the session.
+ *
+ * @param string $key Key of the entry to be checked.
+ * @param array $options Options array. Not used for this adapter method.
+ * @return boolean True if the key exists, false otherwise.
+ */
public function check($key, array $options = array()) {
- return isset($this->_session[$key]);
+ $session =& $this->_session;
+ return function($self, $params) use (&$session) {
+ return isset($session[$params['key']]);
+ };
}
+ /**
+ * Read a value from the session.
+ *
+ * @param null|string $key Key of the entry to be read. If no key is passed, all
+ * current session data is returned.
+ * @param array $options Options array. Not used for this adapter method.
+ * @return mixed Data in the session if successful, false otherwise.
+ */
public function read($key = null, array $options = array()) {
$session = $this->_session;
- return function($self, $params, $chain) use ($session) {
+ return function($self, $params) use ($session) {
extract($params);
if (!$key) {
@@ -45,25 +77,53 @@ class Memory extends \lithium\core\Object {
};
}
+ /**
+ * Write a value to the session.
+ *
+ * @param string $key Key of the item to be stored.
+ * @param mixed $value The value to be stored.
+ * @param array $options Options array. Not used for this adapter method.
+ * @return boolean True on successful write, false otherwise
+ */
public function write($key, $value, array $options = array()) {
$session =& $this->_session;
- return function($self, $params, $chain) use (&$session) {
+ return function($self, $params) use (&$session) {
extract($params);
return (boolean) ($session[$key] = $value);
};
}
+ /**
+ * Delete value from the session
+ *
+ * @param string $key The key to be deleted
+ * @param array $options Options array. Not used for this adapter method.
+ * @return boolean True on successful delete, false otherwise
+ */
public function delete($key, array $options = array()) {
$session =& $this->_session;
- return function($self, $params, $chain) use (&$session) {
+ return function($self, $params) use (&$session) {
extract($params);
unset($session[$key]);
};
}
/**
+ * Clears all keys from the session.
+ *
+ * @param array $options Options array. Not used fro this adapter method.
+ */
+ public function clear(array $options = array()) {
+ $session =& $this->_session;
+
+ return function($self, $params) use (&$session) {
+ $session = array();
+ };
+ }
+
+ /**
* This adapter is always enabled, as it has no external dependencies.
*
* @return boolean True
diff --git a/libraries/lithium/storage/session/adapter/Php.php b/libraries/lithium/storage/session/adapter/Php.php
index 3010725..7599da8 100644
--- a/libraries/lithium/storage/session/adapter/Php.php
+++ b/libraries/lithium/storage/session/adapter/Php.php
@@ -2,12 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\storage\session\adapter;
+use lithium\util\Set;
+use RuntimeException;
+use lithium\core\ConfigException;
+
/**
* A minimal adapter to interface with native PHP sessions.
*
@@ -19,50 +23,64 @@ namespace lithium\storage\session\adapter;
class Php extends \lithium\core\Object {
/**
- * Default ini settings for this session adapter
+ * Default ini settings for this session adapter.
*
* @var array Keys are session ini settings, but without the `session.` namespace.
*/
protected $_defaults = array(
- 'name' => '', 'cookie_lifetime' => '86400', 'cookie_domain' => '',
- 'cookie_secure' => false, 'cookie_httponly' => false
+ 'session.cookie_lifetime' => '0',
);
/**
- * Class constructor
+ * Class constructor.
*
- * Takes care of setting appropriate configurations for
- * this object.
+ * Takes care of setting appropriate configurations for this object.
*
- * @param array $config
+ * @param array $config Unified constructor configuration parameters. You can set
+ * the `session.*` PHP ini settings here as key/value pairs.
* @return void
*/
public function __construct(array $config = array()) {
- parent::__construct((array) $config + $this->_defaults);
+ parent::__construct($config + $this->_defaults);
}
/**
* Initialization of the session.
*
- * @todo Split up into an _initialize() and a _startup().
+ * @todo Split up into an _initialize() and a _start().
* @return void
*/
protected function _init() {
- session_write_close();
+ $config = $this->_config;
+ unset($config['adapter'], $config['strategies'], $config['filters'], $config['init']);
- if (headers_sent()) {
- $_SESSION = (empty($_SESSION)) ?: array();
- } elseif (!isset($_SESSION)) {
- session_cache_limiter("nocache");
+ if (!isset($config['session.name'])) {
+ $config['session.name'] = basename(LITHIUM_APP_PATH);
}
- session_start();
-
- foreach ($this->_defaults as $key => $config) {
- if (isset($this->_config[$key])) {
- ini_set("session.$key", $this->_config[$key]);
+ foreach ($config as $key => $value) {
+ if (strpos($key, 'session.') === false) {
+ continue;
+ }
+ if (ini_set($key, $value) === false) {
+ throw new ConfigException("Could not initialize the session.");
}
}
- $_SESSION['_timestamp'] = time();
+ }
+
+ /**
+ * Starts the session.
+ *
+ * @return boolean True if session successfully started (or has already been started),
+ * false otherwise.
+ */
+ protected static function _start() {
+ if (session_id()) {
+ return true;
+ }
+ if (!isset($_SESSION)) {
+ session_cache_limiter('nocache');
+ }
+ return session_start();
}
/**
@@ -71,28 +89,36 @@ class Php extends \lithium\core\Object {
* @return boolean True if $_SESSION is accessible and if a '_timestamp' key
* has been set, false otherwise.
*/
- public function isStarted() {
- return (isset($_SESSION) && isset($_SESSION['_timestamp']));
+ public static function isStarted() {
+ return (boolean) session_id();
}
/**
- * Obtain the session id.
+ * Sets or obtains the session ID.
*
- * @return mixed Session id, or null if the session has not been started.
+ * @param string $key Optional. If specified, sets the session ID to the value of `$key`.
+ * @return mixed Session ID, or `null` if the session has not been started.
*/
- public function key() {
- return ($id = session_id()) == '' ? null : $id;
+ public static function key($key = null) {
+ if ($key) {
+ return session_id($key);
+ }
+ return session_id() ?: null;
}
/**
* Checks if a value has been set in the session.
*
* @param string $key Key of the entry to be checked.
+ * @param array $options Options array. Not used for this adapter method.
* @return boolean True if the key exists, false otherwise.
*/
- public function check($key) {
- return function($self, $params, $chain) {
- return (isset($_SESSION[$params['key']]));
+ public static function check($key, array $options = array()) {
+ if (!static::isStarted() && !static::_start()) {
+ throw new RuntimeException("Could not start session.");
+ }
+ return function($self, $params) {
+ return Set::check($_SESSION, $params['key']);
};
}
@@ -104,14 +130,26 @@ class Php extends \lithium\core\Object {
* @param array $options Options array. Not used for this adapter method.
* @return mixed Data in the session if successful, false otherwise.
*/
- public function read($key = null, array $options = array()) {
- return function($self, $params, $chain) {
+ public static function read($key = null, array $options = array()) {
+ if (!static::isStarted() && !static::_start()) {
+ throw new RuntimeException("Could not start session.");
+ }
+ return function($self, $params) {
$key = $params['key'];
if (!$key) {
return $_SESSION;
}
- return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
+ if (strpos($key, '.') === false) {
+ return isset($_SESSION[$key]) ? $_SESSION[$key] : null;
+ }
+ $filter = function($keys, $data) use (&$filter) {
+ $key = array_shift($keys);
+ if (isset($data[$key])) {
+ return (empty($keys)) ? $data[$key] : $filter($keys, $data[$key]);
+ }
+ };
+ return $filter(explode('.', $key), $_SESSION);
};
}
@@ -124,8 +162,15 @@ class Php extends \lithium\core\Object {
* @return boolean True on successful write, false otherwise
*/
public static function write($key, $value, array $options = array()) {
- return function($self, $params, $chain) {
- $_SESSION[$params['key']] = $params['value'];
+ if (!static::isStarted() && !static::_start()) {
+ throw new RuntimeException("Could not start session.");
+ }
+ $class = __CLASS__;
+
+ return function($self, $params) use ($class) {
+ return $class::overwrite(
+ $_SESSION, Set::insert($_SESSION, $params['key'], $params['value'])
+ );
};
}
@@ -134,28 +179,67 @@ class Php extends \lithium\core\Object {
*
* @param string $key The key to be deleted
* @param array $options Options array. Not used for this adapter method.
- * @return boolean True on successful delete, false otherwise
+ * @return boolean True if the key no longer exists in the session, false otherwise
*/
public static function delete($key, array $options = array()) {
- return function($self, $params, $chain) {
+ if (!static::isStarted() && !static::_start()) {
+ throw new RuntimeException("Could not start session.");
+ }
+ $class = __CLASS__;
+
+ return function($self, $params) use ($class) {
$key = $params['key'];
+ $class::overwrite($_SESSION, Set::remove($_SESSION, $key));
+ return !Set::check($_SESSION, $key);
+ };
+ }
- if (isset($_SESSION[$key])) {
- unset($_SESSION[$key]);
- return true;
- }
- return false;
+ /**
+ * Clears all keys from the session.
+ *
+ * @param array $options Options array. Not used fro this adapter method.
+ * @return boolean True on successful clear, false otherwise.
+ */
+ public function clear(array $options = array()) {
+ if (!static::isStarted() && !static::_start()) {
+ throw new RuntimeException("Could not start session.");
+ }
+
+ return function($self, $params) {
+ return session_destroy();
};
}
/**
* Determines if PHP sessions are enabled.
*
- * return boolean True if enabled (that is, if session_id() returns a value), false otherwise.
+ * @return boolean True if enabled (that is, if session_id() returns a value), false otherwise.
*/
public static function enabled() {
return (boolean) session_id();
}
+
+ /**
+ * Overwrites session keys and values.
+ *
+ * @param array $old Reference to the array that needs to be overwritten. Will usually
+ * be `$_SESSION`.
+ * @param array $new The data that should overwrite the keys/values in `$old`.
+ * @return true Success
+ */
+ public static function overwrite(&$old, $new) {
+ if (!empty($old)) {
+ foreach ($old as $key => $value) {
+ if (!isset($new[$key])) {
+ unset($old[$key]);
+ }
+ }
+ }
+ foreach ($new as $key => $value) {
+ $old[$key] = $value;
+ }
+ return true;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/session/strategy/Hmac.php b/libraries/lithium/storage/session/strategy/Hmac.php
new file mode 100644
index 0000000..6196f78
--- /dev/null
+++ b/libraries/lithium/storage/session/strategy/Hmac.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\session\strategy;
+
+use RuntimeException;
+use lithium\core\ConfigException;
+use lithium\storage\session\strategy\MissingSignatureException;
+
+/**
+ * This strategy allows you to sign your `Session` and/or `Cookie` data with a passphrase
+ * to ensure that it has not been tampered with.
+ *
+ * Example configuration:
+ *
+ * {{{
+ * Session::config(array(
+ * 'default' => array(
+ * 'adapter' => 'Cookie',
+ * 'strategies' => array('Hmac' => array('secret' => 'foobar'))
+ * )
+ * ));
+ *
+ * This will configure the `HMAC` strategy to be used for all `Session` operations with the
+ * `default` named configuration. A hash-based message authentication code (HMAC) will be
+ * calculated for all data stored in your cookies, and will be compared to the signature
+ * stored in your cookie data. If the two do not match, then your data has been tampered with
+ * (or you have modified the data directly _without_ passing through the `Session` class, which
+ * amounts to the same), then a catchable `RuntimeException` is thrown.
+ *
+ * Please note that this strategy is very finnicky, and is so by design. If you attempt to access
+ * or modify the stored data in any way other than through the `Session` class configured with the
+ * `Hmac` strategy with the properly configured `secret`, then it will probably blow up.
+ *
+ * @link http://en.wikipedia.org/wiki/HMAC Wikipedia: Hash-based Message Authentication Code
+ */
+class Hmac extends \lithium\core\Object {
+
+ /**
+ * The HMAC secret.
+ *
+ * @var string HMAC secret string.
+ */
+ protected static $_secret = null;
+
+ /**
+ * Constructor.
+ *
+ * @param array $config Configuration array. Will throw an exception if the 'secret'
+ * configuration key is not set.
+ * @return void
+ */
+ public function __construct(array $config = array()) {
+ if (!isset($config['secret'])) {
+ throw new ConfigException("HMAC strategy requires a secret key.");
+ }
+ static::$_secret = $config['secret'];
+ }
+
+ /**
+ * Write strategy method.
+ * Adds an HMAC signature to the data. Note that this will transform the
+ * passed `$data` to an array, and add a `__signature` key with the HMAC-caculated
+ * value.
+ *
+ * @see lithium\storage\Session
+ * @see lithium\core\Adaptable::config()
+ * @link http://php.net/manual/en/function.hash-hmac.php PHP Manual: hash_hmac()
+ * @param mixed $data The data to be signed.
+ * @param array $options Options for this method.
+ * @return array Data & signature.
+ */
+ public function write($data, array $options = array()) {
+ $class = $options['class'];
+
+ $futureData = $class::read(null, array('strategies' => false));
+ $futureData += array($options['key'] => $data);
+ unset($futureData['__signature']);
+
+ $signature = static::_signature($futureData);
+ $class::write('__signature', $signature, array('strategies' => false) + $options);
+ return $data;
+ }
+
+ /**
+ * Read strategy method.
+ * Validates the HMAC signature of the stored data. If the signatures match, then
+ * the data is safe, and the 'valid' key in the returned data will be
+ *
+ * If the store being read does not contain a `__signature` field, a `MissingSignatureException`
+ * is thrown. When catching this exception, you may choose to handle it by either writing
+ * out a signature (e.g. in cases where you know that no pre-existing signature may exist), or
+ * you can blackhole it as a possible tampering attempt.
+ *
+ * @param array $data the Data being read.
+ * @param array $options Options for this method.
+ * @return array validated data
+ */
+ public function read($data, array $options = array()) {
+ $class = $options['class'];
+
+ $currentData = $class::read(null, array('strategies' => false));
+
+ if (!isset($currentData['__signature'])) {
+ throw new MissingSignatureException('HMAC signature not found.');
+ }
+ $currentSignature = $currentData['__signature'];
+ $signature = static::_signature($currentData);
+
+ if ($signature !== $currentSignature) {
+ $message = "Possible data tampering: HMAC signature does not match data.";
+ throw new RuntimeException($message);
+ }
+ return $data;
+ }
+
+ /**
+ * Delete strategy method.
+ *
+ * @see lithium\storage\Session
+ * @see lithium\core\Adaptable::config()
+ * @link http://php.net/manual/en/function.hash-hmac.php PHP Manual: hash_hmac()
+ * @param mixed $data The data to be signed.
+ * @param array $options Options for this method.
+ * @return array Data & signature.
+ */
+ public function delete($data, array $options = array()) {
+ $class = $options['class'];
+
+ $futureData = $class::read(null, array('strategies' => false));
+ unset($futureData[$options['key']]);
+
+ $signature = static::_signature($futureData);
+ $class::write('__signature', $signature, array('strategies' => false) + $options);
+ return $data;
+ }
+
+ /**
+ * Calculate the HMAC signature based on the data and a secret key.
+ *
+ * @param mixed $data
+ * @param null|string $secret Secret key for HMAC signature creation.
+ * @return string HMAC signature.
+ */
+ protected static function _signature($data, $secret = null) {
+ unset($data['__signature']);
+ $secret = ($secret) ?: static::$_secret;
+ return hash_hmac('sha1', serialize($data), $secret);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/session/strategy/MissingSignatureException.php b/libraries/lithium/storage/session/strategy/MissingSignatureException.php
new file mode 100644
index 0000000..a80a97b
--- /dev/null
+++ b/libraries/lithium/storage/session/strategy/MissingSignatureException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\session\strategy;
+
+/**
+ * A `MissingSignatureException` may be thrown when reading data from a session-based storage that
+ * is expecting an HMAC signature, but none is found..
+ */
+class MissingSignatureException extends \RuntimeException {
+
+ protected $code = 403;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/Helper.php b/libraries/lithium/template/Helper.php
index aa17402..339a465 100644
--- a/libraries/lithium/template/Helper.php
+++ b/libraries/lithium/template/Helper.php
@@ -2,14 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\template;
-use \lithium\util\String;
+use lithium\util\String;
+/**
+ * Abstract class for template helpers to extend.
+ * Supplies the basic functionality of _render and _options,
+ * as well as escaping.
+ *
+ */
abstract class Helper extends \lithium\core\Object {
/**
@@ -27,8 +33,26 @@ abstract class Helper extends \lithium\core\Object {
*/
protected $_strings = array();
+ /**
+ * The Renderer object this Helper is bound to.
+ *
+ * @var lithium\template\view\Renderer
+ * @see lithium\template\view\Renderer
+ */
protected $_context = null;
+ /**
+ * This property can be overwritten with any class dependencies a helper subclass has.
+ *
+ * @var array
+ */
+ protected $_classes = array();
+
+ /**
+ * Auto configuration properties.
+ *
+ * @var array
+ */
protected $_autoConfig = array('classes' => 'merge', 'context');
/**
@@ -38,7 +62,7 @@ abstract class Helper extends \lithium\core\Object {
*/
protected $_minimized = array(
'compact', 'checked', 'declare', 'readonly', 'disabled', 'selected', 'defer', 'ismap',
- 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize'
+ 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize', 'async'
);
public function __construct(array $config = array()) {
@@ -100,33 +124,70 @@ abstract class Helper extends \lithium\core\Object {
return array($scope, $options);
}
+ /**
+ * Render a string template after applying context filters
+ * Use examples in the Html::link() method:
+ * `return $this->_render(__METHOD__, 'link', compact('title', 'url', 'options'), $scope);`
+ *
+ * @param string $method name of method that is calling the render (for context filters)
+ * @param string $string template key (in Helper::_strings) to render
+ * @param array $params associatied array of template inserts {:key} will be replaced by value
+ * @param array $options
+ * @return string Rendered HTML
+ */
protected function _render($method, $string, $params, array $options = array()) {
- foreach ($params as $key => $value) {
- $params[$key] = $this->_context->applyHandler($this, $method, $key, $value, $options);
+ $strings = $this->_strings;
+
+ if ($this->_context) {
+ foreach ($params as $key => $value) {
+ $params[$key] = $this->_context->applyHandler(
+ $this, $method, $key, $value, $options
+ );
+ }
+ $strings = $this->_context->strings();
}
- $strings = $this->_context ? $this->_context->strings() : $this->_strings;
return String::insert(isset($strings[$string]) ? $strings[$string] : $string, $params);
}
+ /**
+ * Convert a set of options to HTML attributes
+ *
+ * @param array $params
+ * @param string $method
+ * @param array $options
+ * @return string
+ */
protected function _attributes($params, $method = null, array $options = array()) {
if (!is_array($params)) {
- return empty($params) ? '' : ' ' . $params;
+ return !$params ? '' : ' ' . $params;
}
$defaults = array('escape' => true, 'prepend' => ' ', 'append' => '');
$options += $defaults;
$result = array();
+
foreach ($params as $key => $value) {
- $result[] = $this->_formatAttr($key, $value, $options);
+ $result[] = $this->_attribute($key, $value, $options);
}
return $result ? $options['prepend'] . implode(' ', $result) . $options['append'] : '';
}
- protected function _formatAttr($key, $value, array $options = array()) {
+ /**
+ * Convert a key/value pair to a valid HTML attribute.
+ *
+ * @param string $key The key name of the HTML attribute.
+ * @param mixed $value The HTML attribute value.
+ * @param array $options The options used when converting the key/value pair to attributes:
+ * - `'escape'` _boolean_: Indicates whether `$key` and `$value` should be
+ * HTML-escaped. Defaults to `true`.
+ * - `'format'` _string_: The format string. Defaults to `'%s="%s"'`.
+ * @return string Returns an HTML attribute/value pair, in the form of `'$key="$value"'`.
+ */
+ protected function _attribute($key, $value, array $options = array()) {
$defaults = array('escape' => true, 'format' => '%s="%s"');
$options += $defaults;
if (in_array($key, $this->_minimized)) {
- $isMini = ($value == 1 || $value === true || $value === 'true' || $value == $key);
+ $isMini = ($value == 1 || $value === true || $value == $key);
if (!($value = $isMini ? $key : $value)) {
return null;
}
diff --git a/libraries/lithium/template/TemplateException.php b/libraries/lithium/template/TemplateException.php
new file mode 100644
index 0000000..bd9ae62
--- /dev/null
+++ b/libraries/lithium/template/TemplateException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\template;
+
+/**
+ * A `TemplateException` is thrown whenever a view template cannot be found, or a called template is
+ * not readible or accessible for rendering. Also used by the view compiler if a compiled template
+ * cannot be written.
+ */
+class TemplateException extends \RuntimeException {
+
+ protected $code = 500;
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/View.php b/libraries/lithium/template/View.php
index 48eb0bc..05a6c1f 100644
--- a/libraries/lithium/template/View.php
+++ b/libraries/lithium/template/View.php
@@ -2,17 +2,85 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\template;
-use \RuntimeException;
-use \lithium\core\Libraries;
+use lithium\core\Libraries;
+use lithium\template\TemplateException;
+/**
+ * As one of the three pillars of the Model-View-Controller design pattern, the `View` class
+ * (along with other supporting classes) is responsible for taking the data passed from the
+ * request and/or controller, inserting this into the requested template/layout, and then returning
+ * the rendered content.
+ *
+ * The `View` class interacts with a variety of other classes in order to achieve maximum
+ * flexibility and configurability at all points in the view rendering and presentation
+ * process. The `Loader` class is tasked with locating and reading template files which are then
+ * passed to the `Renderer` adapter subclass.
+ *
+ * In the default configuration, the `File` adapter acts as both renderer and loader, loading files
+ * from paths defined in _process steps_ (described below) and rendering them as plain PHP files,
+ * augmented with [special syntax](../template).
+ *
+ * The `View` class operates on _processes_, which define the steps to render a completed view. For
+ * example, the default process, which renders a template wrapped in a layout, is comprised of two
+ * _steps_: the first step renders the main template and captures it to the rendering context, where
+ * it is embedded in the layout in the second step. See the `$_steps` and `$_processes` properties
+ * for more information.
+ *
+ * Using steps and processes, you can create rendering scenarios to suit very complex needs.
+ *
+ * By default, the `View` class is called during the course of the framework's dispatch cycle by the
+ * `Media` class. However, it is also possible to instantiate and call `View` directly, in cases
+ * where you wish to bypass all other parts of the framework and simply return rendered content.
+ *
+ * A simple example, using the `Simple` renderer/loader for string templates:
+ *
+ * {{{
+ * $view = new View(array('loader' => 'Simple', 'renderer' => 'Simple'));
+ * echo $view->render('element', array('name' => "Robert"), array('element' => 'Hello, {:name}!'));
+ *
+ * // Output:
+ * "Hello, Robert!";
+ * }}}
+ *
+ * _Note_: This is easily adapted for XML templating.
+ *
+ * Another example, this time of something that could be used in an appliation
+ * error handler:
+ *
+ * {{{
+ * $view = new View(array(
+ * 'paths' => array(
+ * 'template' => '{:library}/views/errors/{:template}.{:type}.php',
+ * 'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
+ * )
+ * ));
+ *
+ * $page = $View->render('all', array('content' => $info), array(
+ * 'template' => '404',
+ * 'layout' => 'error'
+ * ));
+ * }}}
+ *
+ * To learn more about processes and process steps, see the `$_processes` and `$_steps` properties,
+ * respectively.
+ *
+ * @see lithium\template\view\Renderer
+ * @see lithium\template\view\adapter
+ * @see lithium\net\http\Media
+ */
class View extends \lithium\core\Object {
+ /**
+ * Output filters for view rendering.
+ *
+ * @var array List of filters.
+ */
public $outputFilters = array();
/**
@@ -20,72 +88,271 @@ class View extends \lithium\core\Object {
* applicable. May be empty if this does not apply. For example, if the View class is
* created to render an email.
*
- * @var object Request object instance.
* @see lithium\action\Request
+ * @var object `Request` object instance.
*/
protected $_request = null;
+ /**
+ * Holds a reference to the `Response` object that will be returned at the end of the current
+ * dispatch cycle. Allows headers and other response attributes to be assigned in the templating
+ * layer.
+ *
+ * @see lithium\action\Response
+ * @var object `Response` object instance.
+ */
+ protected $_response = null;
+
+ /**
+ * The object responsible for loading template files.
+ *
+ * @var object Loader object.
+ */
protected $_loader = null;
+ /**
+ * Object responsible for rendering output.
+ *
+ * @var objet Renderer object.
+ */
protected $_renderer = null;
- protected $_autoConfig = array('request');
+ /**
+ * View processes are aggregated lists of steps taken to to create a complete, rendered view.
+ * For example, the default process, `'all'`, renders a template, then renders a layout, using
+ * the rendered template content. A process can be defined using one or more steps defined in
+ * the `$_steps` property. Each process definition is a simple array of ordered values, where
+ * each value is a key in the `$_steps` array.
+ *
+ * @see lithium\template\View::$_steps
+ * @see lithium\template\View::render()
+ * @var array
+ */
+ protected $_processes = array(
+ 'all' => array('template', 'layout'),
+ 'template' => array('template'),
+ 'element' => array('element')
+ );
+
+ /**
+ * The list of available rendering steps. Each step contains instructions for how to render one
+ * piece of a multi-step view rendering. The `View` class combines multiple steps into
+ * _processes_ to create the final output.
+ *
+ * Each step is named by its key in the `$_steps` array, and can have the following options:
+ *
+ * - `'path'` _string_: Indicates the set of paths to use when loading templates.
+ *
+ * - `'conditions'` _mixed_: Make the step dependent on a value being present, or on some other
+ * arbitrary condition. If a `'conditions'` is a string, it indicates that a key with that
+ * name must be present in the `$options` passed to `render()`, and must be set to a
+ * non-empty value. If a closure, it will be executed with the rendering parameters, and must
+ * return `true` or `false`. In either case, if the condition is satisfied, the step is
+ * processed. Otherwise, it is skipped. See the `_conditions()` method for more information.
+ *
+ * - `'capture'` _array_: If specified, allows the results of this rendering step to be assigned
+ * to a template variable used in subsequent steps, or to the templating context for use in
+ * subsequent steps. If can be specified in the form of `array('context' => '<var-name>')` or
+ * `array('data' => '<var-name>')`. If the `'context'` key is used, the results are captured
+ * to the rendering context. Likewise with the `'data'` key, results are captured to a
+ * template variable.
+ *
+ * - `'multi'` _boolean_: If set to `true`, the rendering parameter matching the name of this
+ * step can be an array containing multiple values, in which case this step is executed
+ * multiple times, once for each value of the array.
+ *
+ * @see lithium\template\View::$_processes
+ * @see lithium\template\View::render()
+ * @var array
+ */
+ protected $_steps = array(
+ 'template' => array('path' => 'template', 'capture' => array('context' => 'content')),
+ 'layout' => array(
+ 'path' => 'layout', 'conditions' => 'layout', 'multi' => true, 'capture' => array(
+ 'context' => 'content'
+ )
+ ),
+ 'element' => array('path' => 'element')
+ );
+
+ /**
+ * Auto-configuration parameters.
+ *
+ * @var array Objects to auto-configure.
+ */
+ protected $_autoConfig = array(
+ 'request', 'response', 'processes' => 'merge', 'steps' => 'merge'
+ );
+ /**
+ * Constructor.
+ *
+ * @param array $config Configuration parameters.
+ * The available options are:
+ * - `'loader'` _mixed_: For locating/reading view, layout and element
+ * templates. Defaults to `File`.
+ * - `'renderer'` _mixed_: Populates the view/layout with the data set from the
+ * controller. Defaults to `'File'`.
+ * - `request`: The request object to be made available in the view. Defalts to `null`.
+ * - `vars`: Defaults to `array()`.
+ * @return void
+ */
public function __construct(array $config = array()) {
$defaults = array(
'request' => null,
+ 'response' => null,
'vars' => array(),
'loader' => 'File',
'renderer' => 'File',
+ 'steps' => array(),
+ 'processes' => array(),
'outputFilters' => array()
);
parent::__construct($config + $defaults);
}
+ /**
+ * Perform initialization of the View.
+ *
+ * @return void
+ */
protected function _init() {
parent::_init();
+
foreach (array('loader', 'renderer') as $key) {
if (is_object($this->_config[$key])) {
$this->{'_' . $key} = $this->_config[$key];
continue;
}
+ $class = $this->_config[$key];
+ $config = array('view' => $this) + $this->_config;
+ $this->{'_' . $key} = Libraries::instance('adapter.template.view', $class, $config);
+ }
+ $encoding = 'UTF-8';
- if (!$class = Libraries::locate('adapter.template.view', $this->_config[$key])) {
- throw new RuntimeException("Template adapter {$this->_config[$key]} not found");
- }
- $this->{'_' . $key} = new $class(array('view' => $this) + $this->_config);
+ if ($this->_response) {
+ $encoding =& $this->_response->encoding;
}
- $h = function($data) { return htmlspecialchars((string) $data); };
+ $h = function($data) use (&$encoding) {
+ return htmlspecialchars((string) $data, ENT_QUOTES, $encoding);
+ };
$this->outputFilters += compact('h') + $this->_config['outputFilters'];
}
- public function render($type, $data = array(), array $options = array()) {
- $defaults = array('context' => array(), 'type' => 'html', 'layout' => null);
+ public function render($process, array $data = array(), array $options = array()) {
+ $defaults = array(
+ 'type' => 'html',
+ 'layout' => null,
+ 'template' => null,
+ 'context' => array(),
+ );
$options += $defaults;
- if (is_array($type)) {
- list($type, $template) = each($type);
+ $data += isset($options['data']) ? (array) $options['data'] : array();
+ $paths = isset($options['paths']) ? (array) $options['paths'] : array();
+ unset($options['data'], $options['paths']);
+ $params = array_filter($options, function($val) { return $val && is_string($val); });
+ $result = null;
+
+ foreach ($this->_process($process, $params) as $name => $step) {
+ if (!$this->_conditions($step, $params, $data, $options)) {
+ continue;
+ }
+ if ($step['multi'] && isset($options[$name])) {
+ foreach ((array) $options[$name] as $value) {
+ $params[$name] = $value;
+ $result = $this->_step($step, $params, $data, $options);
+ }
+ continue;
+ }
+ $result = $this->_step((array) $step, $params, $data, $options);
+ }
+ return $result;
+ }
+
+ protected function _conditions($step, $params, $data, $options) {
+ if (!$conditions = $step['conditions']) {
+ return true;
+ }
+ if (is_callable($conditions) && !$conditions($params, $data, $options)) {
+ return false;
+ }
+ if (is_string($conditions) && !(isset($options[$conditions]) && $options[$conditions])) {
+ return false;
+ }
+ return true;
+ }
+
+ protected function _step(array $step, array $params, array &$data, array &$options = array()) {
+ $step += array('path' => null, 'capture' => null);
+ $_renderer = $this->_renderer;
+ $_loader = $this->_loader;
+ $filters = $this->outputFilters;
+ $params = compact('step', 'params', 'options') + array('data' => $data + $filters);
+
+ $filter = function($self, $params) use (&$_renderer, &$_loader) {
+ $template = $_loader->template($params['step']['path'], $params['params']);
+ return $_renderer->render($template, $params['data'], $params['options']);
+ };
+ $result = $this->_filter(__METHOD__, $params, $filter);
+
+ if (is_array($step['capture'])) {
+ switch (key($step['capture'])) {
+ case 'context':
+ $options['context'][current($step['capture'])] = $result;
+ break;
+ case 'data':
+ $data[current($step['capture'])] = $result;
+ break;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Converts a process name to an array containing the rendering steps to be executed for each
+ * process.
+ *
+ * @param string $process A named set of rendering steps.
+ * @param array $params
+ * @return array A 2-dimensional array that defines the rendering process. The first dimension
+ * is a numerically-indexed array containing each rendering step. The second dimension
+ * represents the parameters for each step.
+ */
+ protected function _process($process, &$params) {
+ $defaults = array('conditions' => null, 'multi' => false);
+
+ if (!is_array($process)) {
+ if (!isset($this->_processes[$process])) {
+ throw new TemplateException("Undefined rendering process '{$process}'.");
+ }
+ $process = $this->_processes[$process];
+ }
+ if (is_string(key($process))) {
+ return $this->_convertSteps($process, $params, $defaults);
}
+ $result = array();
- switch ($type) {
- case 'all':
- $content = $this->render('template', $data, $options);
+ foreach ($process as $step) {
+ if (is_array($step)) {
+ $result[] = $step + $defaults;
+ continue;
+ }
+ if (!isset($this->_steps[$step])) {
+ throw new TemplateException("Undefined rendering step '{$step}'.");
+ }
+ $result[$step] = $this->_steps[$step] + $defaults;
+ }
+ return $result;
+ }
- if (!$options['layout']) {
- return $content;
- }
- $options['context'] += compact('content');
- return $this->render('layout', $data, $options);
- case 'element':
- $options = compact('template') + array('controller' => 'elements') + $options;
- $type = 'template';
- case 'template':
- case 'layout':
- $template = $this->_loader->template($type, $options);
- $data = (array) $data + $this->outputFilters;
- return $this->_renderer->render($template, $data, $options);
+ protected function _convertSteps($command, &$params, $defaults) {
+ if (count($command) == 1) {
+ $params['template'] = current($command);
+ return array(array('path' => key($command)) + $defaults);
}
+ return $command;
}
}
diff --git a/libraries/lithium/template/helper/Form.php b/libraries/lithium/template/helper/Form.php
index 41206b7..38280e5 100644
--- a/libraries/lithium/template/helper/Form.php
+++ b/libraries/lithium/template/helper/Form.php
@@ -2,24 +2,25 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\template\helper;
-use \lithium\util\Set;
-use \lithium\util\Inflector;
+use lithium\util\Set;
+use lithium\util\Inflector;
+use UnexpectedValueException;
/**
* A helper class to facilitate generating, processing and securing HTML forms. By default, `Form`
* will simply generate HTML forms and widgets, but by creating a form with a _binding object_,
* the helper can pre-fill form input values, render error messages, and introspect column types.
*
- * For example, assuming you have created a `Post` model in your application:
+ * For example, assuming you have created a `Posts` model in your application:
* {{{// In controller code:
- * use \app\models\Post;
- * $post = Post::find(1);
+ * use app\models\Posts;
+ * $post = Posts::find(1);
* return compact('post');
*
* // In view code:
@@ -44,18 +45,20 @@ class Form extends \lithium\template\Helper {
'checkbox-multi-start' => '',
'error' => '<div{:options}>{:content}</div>',
'errors' => '{:content}',
+ 'element' => '<input type="{:type}" name="{:name}"{:options} />',
'file' => '<input type="file" name="{:name}"{:options} />',
- 'form' => '<form action="{:url}"{:options}>{:content}',
+ 'form' => '<form action="{:url}"{:options}>{:append}',
'form-end' => '</form>',
'hidden' => '<input type="hidden" name="{:name}"{:options} />',
'field' => '<div{:wrap}>{:label}{:input}{:error}</div>',
'field-checkbox' => '<div{:wrap}>{:input}{:label}{:error}</div>',
+ 'field-radio' => '<div{:wrap}>{:input}{:label}{:error}</div>',
'label' => '<label for="{:name}"{:options}>{:title}</label>',
'legend' => '<legend>{:content}</legend>',
'option-group' => '<optgroup label="{:label}"{:options}>',
'option-group-end' => '</optgroup>',
'password' => '<input type="password" name="{:name}"{:options} />',
- 'radio' => '<input type="radio" name="{:name}" id="{:id}"{:options} />{:label}',
+ 'radio' => '<input type="radio" name="{:name}" {:options} />',
'select-start' => '<select name="{:name}"{:options}>',
'select-multi-start' => '<select name="{:name}[]"{:options}>',
'select-empty' => '<option value=""{:options}> </option>',
@@ -110,21 +113,61 @@ class Form extends \lithium\template\Helper {
* the keys match keys from `schema()`, and the values are either strings (in cases
* where a field only has one error) or an array (in case of multiple errors),
*
- * For an example of how to implement these methods, see the `lithium\data\model\Record` object.
+ * For an example of how to implement these methods, see the `lithium\data\Entity` object.
*
- * @var mixed A single data object, a `Collection` of multiple data ovjects, or an array of data
- * objects/`Collection`s.
- * @see lithium\data\model\Record
+ * @see lithium\data\Entity
+ * @see lithium\data\Collection
* @see lithium\template\helper\Form::create()
+ * @var mixed A single data object, a `Collection` of multiple data objects, or an array of data
+ * objects/`Collection`s.
*/
protected $_binding = null;
+ /**
+ * Array of options used to create the form to which `$_binding` is currently bound.
+ * Overwritten when `end()` is called.
+ *
+ * @var array
+ */
+ protected $_bindingOptions = array();
+
public function __construct(array $config = array()) {
+ $self =& $this;
+
$defaults = array(
- 'base' => array(), 'text' => array(), 'textarea' => array(),
- 'select' => array('multiple' => false)
+ 'base' => array(),
+ 'text' => array(),
+ 'textarea' => array(),
+ 'select' => array('multiple' => false),
+ 'attributes' => array(
+ 'id' => function($method, $name, $options) use (&$self) {
+ if (in_array($method, array('create', 'end', 'label', 'error'))) {
+ return;
+ }
+ if (!$name || ($method == 'hidden' && $name == '_method')) {
+ return;
+ }
+ $id = Inflector::camelize(Inflector::slug($name));
+ $model = ($binding = $self->binding()) ? $binding->model() : null;
+ return $model ? basename(str_replace('\\', '/', $model)) . $id : $id;
+ }
+ )
);
- parent::__construct((array) $config + $defaults);
+ parent::__construct(Set::merge($defaults, $config));
+ }
+
+ /**
+ * Object initializer. Adds a content handler for the `wrap` key in the `field()` method, which
+ * converts an array of properties to an attribute string.
+ *
+ * @return void
+ */
+ protected function _init() {
+ parent::_init();
+
+ if ($this->_context) {
+ $this->_context->handlers(array('wrap' => '_attributes'));
+ }
}
/**
@@ -138,17 +181,37 @@ class Form extends \lithium\template\Helper {
* $this->form->config(array('label' => array('class' => 'foo')));
* }}}
*
+ * Note that this can be overridden on a case-by-case basis, and when overridding, values are
+ * not merged or combined. Therefore, if you wanted a particular `<label />` to have both `foo`
+ * and `bar` as classes, you would have to specify `'class' => 'foo bar'`.
+ *
+ * You can also use this method to change the string template that a method uses to render its
+ * content. For example, the default template for rendering a checkbox is
+ * `'<input type="checkbox" name="{:name}"{:options} />'`. However, suppose you implemented your
+ * own custom UI elements, and you wanted to change the markup used, you could do the following:
+ *
+ * {{{
+ * $this->form->config(array('templates' => array(
+ * 'checkbox' => '<div id="{:name}" class="ui-checkbox-element"{:options}></div>'
+ * )));
+ * }}}
+ *
+ * Now, for any calls to `$this->form->checkbox()`, your custom markup template will be applied.
+ * This works for any `Form` method that renders HTML elements.
+ *
* @see lithium\template\helper\Form::$_templateMap
- * @param array $config An associative array where the keys are `Form` method names, and the
+ * @param array $config An associative array where the keys are `Form` method names (or
+ * `'templates'`, to include a template-overriding sub-array), and the
* values are arrays of configuration options to be included in the `$options`
* parameter of each method specified.
* @return array Returns an array containing the currently set per-method configurations, and
* an array of the currently set template overrides (in the `'templates'` array key).
*/
public function config(array $config = array()) {
- if (empty($config)) {
+ if (!$config) {
+ $keys = array('base' => '', 'text' => '', 'textarea' => '', 'attributes' => '');
return array('templates' => $this->_templateMap) + array_intersect_key(
- $this->_config, array('base' => '', 'text' => '', 'textarea' => '')
+ $this->_config, $keys
);
}
if (isset($config['templates'])) {
@@ -168,64 +231,90 @@ class Form extends \lithium\template\Helper {
* `lithium\template\helper\Form::$_binding`.
*
* @see lithium\template\helper\Form::$_binding
- * @see lithium\data\model\Record
- * @param object $binding
- * @param array $options
+ * @see lithium\data\Entity
+ * @param object $binding The object to bind the form to. This is usually an instance of
+ * `Record` or `Document`, or some other class that extends
+ * `lithium\data\Entity`.
+ * @param array $options Other parameters for creating the form. Available options are:
+ * - `'url'` _mixed_: A string URL or URL array parameters defining where in the
+ * application the form should be submitted to.
+ * - `'action'` _string_: This is a shortcut to be used if you wish to only
+ * specify the name of the action to submit to, and use the default URL
+ * parameters (i.e. the current controller, etc.) for generating the remainder
+ * of the URL. Ignored if the `'url'` key is set.
+ * - `'type'` _string_: Currently the only valid option is `'file'`. Set this if
+ * the form will be used for file uploads.
+ * - `'method'` _string_: Represents the HTTP method with which the form will be
+ * submitted (`'get'`, `'post'`, `'put'` or `'delete'`). If `'put'` or
+ * `'delete'`, the request method is simulated using a hidden input field.
* @return string Returns a `<form />` open tag with the `action` attribute defined by either
* the `'action'` or `'url'` options (defaulting to the current page if none is
* specified), the HTTP method is defined by the `'method'` option, and any HTML
* attributes passed in `$options`.
*/
public function create($binding = null, array $options = array()) {
+ $request = $this->_context ? $this->_context->request() : null;
+
$defaults = array(
- 'url' => $this->_context->request()->params,
+ 'url' => $request ? $request->params : array(),
'type' => null,
'action' => null,
'method' => $binding ? ($binding->exists() ? 'put' : 'post') : 'post'
);
- list(, $options, $template) = $this->_defaults(__FUNCTION__, null, $options);
+
+ list(, $options, $tpl) = $this->_defaults(__FUNCTION__, null, $options);
list($scope, $options) = $this->_options($defaults, $options);
$_binding =& $this->_binding;
- $method = __METHOD__;
+ $_options =& $this->_bindingOptions;
$params = compact('scope', 'options', 'binding');
+ $extra = array('method' => __METHOD__) + compact('tpl', 'defaults');
- $filter = function($self, $params, $chain) use ($method, $template, $defaults, &$_binding) {
+ $filter = function($self, $params) use ($extra, &$_binding, &$_options) {
$scope = $params['scope'];
$options = $params['options'];
$_binding = $params['binding'];
- $content = null;
+ $append = null;
+ $scope['method'] = strtolower($scope['method']);
- if (!in_array(strtolower($scope['method']), array('get', 'post'))) {
- $content = $self->hidden('_method', array(
- 'name' => '_method', 'value' => strtoupper($scope['method'])
- ));
- }
if ($scope['type'] == 'file') {
- if (strtolower($scope['method']) == 'get') {
+ if ($scope['method'] == 'get') {
$scope['method'] = 'post';
}
$options['enctype'] = 'multipart/form-data';
}
+ if (!($scope['method'] == 'get' || $scope['method'] == 'post')) {
+ $append = $self->hidden('_method', array('value' => strtoupper($scope['method'])));
+ $scope['method'] = 'post';
+ }
+
$url = $scope['action'] ? array('action' => $scope['action']) : $scope['url'];
- $options['method'] = strtoupper($scope['method']);
+ $options['method'] = strtolower($scope['method']);
+ $args = array($extra['method'], $extra['tpl'], compact('url', 'options', 'append'));
+ $_options = $scope + $options;
- return $self->invokeMethod('_render', array(
- $method, $template, compact('url', 'content', 'options')
- ));
+ return $self->invokeMethod('_render', $args);
};
- return $this->_filter($method, $params, $filter);
+ return $this->_filter(__METHOD__, $params, $filter);
}
+ /**
+ * Echoes a closing `</form>` tag and unbinds the `Form` helper from any `Record` or `Document`
+ * object used to generate the corresponding form.
+ *
+ * @return string Returns a closing `</form>` tag.
+ */
public function end() {
list(, $options, $template) = $this->_defaults(__FUNCTION__, null, array());
$params = compact('options', 'template');
$_binding =& $this->_binding;
$_context =& $this->_context;
+ $_options =& $this->_bindingOptions;
- $filter = function($self, $params, $chain) use (&$_binding, &$_context) {
+ $filter = function($self, $params) use (&$_binding, &$_context, &$_options) {
unset($_binding);
+ $_options = array();
return $_context->strings('form-end');
};
$result = $this->_filter(__METHOD__, $params, $filter);
@@ -235,23 +324,88 @@ class Form extends \lithium\template\Helper {
}
/**
+ * Returns the entity that the `Form` helper is currently bound to.
+ *
+ * @see lithium\template\helper\Form::$_binding
+ * @return object Returns an object, usually an instance of `lithium\data\Entity`.
+ */
+ public function binding() {
+ return $this->_binding;
+ }
+
+ /**
+ * Implements alternative input types as method calls against `Form` helper. Enables the
+ * generation of HTML5 input types and other custom input types:
+ *
+ * {{{ embed:lithium\tests\cases\template\helper\FormTest::testCustomInputTypes(1-2) }}}
+ *
+ * @param string $type The method called, which represents the `type` attribute of the
+ * `<input />` tag.
+ * @param array $params An array of method parameters passed to the method call. The first
+ * element should be the name of the input field, and the second should be an array
+ * of element attributes.
+ * @return string Returns an `<input />` tag of the type specified in `$type`.
+ */
+ public function __call($type, array $params = array()) {
+ $params += array(null, array());
+ list($name, $options) = $params;
+ list($name, $options, $template) = $this->_defaults($type, $name, $options);
+ $template = $this->_context->strings($template) ? $template : 'element';
+ return $this->_render($type, $template, compact('type', 'name', 'options', 'value'));
+ }
+
+ /**
* Generates a form field with a label, input, and error message (if applicable), all contained
* within a wrapping element.
*
- * @param string $name The name of the field to render. If the form was bound to an object
- * passed in `create()`, `$name` should be the field name of a
- * @param array $options Rendering options for the form field.
+ * {{{
+ * echo $this->form->field('name');
+ * echo $this->form->field('present', array('type' => 'checkbox'));
+ * echo $this->form->field(array('email' => 'Enter a valid email'));
+ * echo $this->form->field(array('name','email','phone'),array('div' => false));
+ * }}}
+ * @param mixed $name The name of the field to render. If the form was bound to an object
+ * passed in `create()`, `$name` should be the name of a field in that object.
+ * Otherwise, can be any arbitrary field name, as it will appear in POST data.
+ * Alternatively supply an array of fields that will use the same options
+ * array($field1 => $label1, $field2, $field3 => $label3)
+ * @param array $options Rendering options for the form field. The available options are as
+ * follows:
+ * - `'label'` _mixed_: A string or array defining the label text and / or
+ * parameters. By default, the label text is a human-friendly version of `$name`.
+ * However, you can specify the label manually as a string, or both the label
+ * text and options as an array, i.e.:
+ * `array('label text' => array('class' => 'foo', 'any' => 'other options'))`.
+ * - `'type'` _string_: The type of form field to render. Available default options
+ * are: `'text'`, `'textarea'`, `'select'`, `'checkbox'`, `'password'` or
+ * `'hidden'`, as well as any arbitrary type (i.e. HTML5 form fields).
+ * - `'template'` _string_: Defaults to `'template'`, but can be set to any named
+ * template string, or an arbitrary HTML fragment. For example, to change the
+ * default wrapper tag from `<div />` to `<li />`, you can pass the following:
+ * `'<li{:wrap}>{:label}{:input}{:error}</li>'`.
+ * - `'wrap'` _array_: An array of HTML attributes which will be embedded in the
+ * wrapper tag.
+ * - `list` _array_: If `'type'` is set to `'select'`, `'list'` is an array of
+ * key/value pairs representing the `$list` parameter of the `select()` method.
* @return string Returns a form input (the input type is based on the `'type'` option), with
* label and error message, wrapped in a `<div />` element.
*/
public function field($name, array $options = array()) {
+ if (is_array($name)) {
+ return $this->_fields($name, $options);
+ }
$defaults = array(
'label' => null,
- 'type' => 'text',
+ 'type' => isset($options['list']) ? 'select' : 'text',
'template' => 'field',
- 'wrap' => null,
- 'list' => null
+ 'wrap' => array(),
+ 'list' => null,
);
+ $type = isset($options['type']) ? $options['type'] : $defaults['type'];
+
+ if ($this->_context->strings('field-' . $type)) {
+ $defaults['template'] = 'field-' . $type;
+ }
list($options, $fieldOptions) = $this->_options($defaults, $options);
list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
@@ -262,29 +416,52 @@ class Form extends \lithium\template\Helper {
$type = $options['type'];
$label = $input = null;
- if ($options['label'] === null || !empty($options['label'])) {
- $label = $this->label($name, $options['label']);
+ if (($options['label'] === null || $options['label']) && $options['type'] != 'hidden') {
+ $for = isset($options['id']) ? $options['id'] : '';
+ $label = $options['label'] ?: $options['label'] = Inflector::humanize($name);
+ $label = $this->label($for, $label);
}
switch (true) {
case ($type == 'select'):
$input = $this->select($name, $options['list'], $fieldOptions);
break;
- case (method_exists($this, $type)):
+ default:
$input = $this->{$type}($name, $fieldOptions);
break;
}
$error = ($this->_binding) ? $this->error($name) : null;
- $params = compact('wrap', 'label', 'input', 'error');
+ return $this->_render(__METHOD__, $template, compact('wrap', 'label', 'input', 'error'));
+ }
+
+ /**
+ * Helper method used by `Form::field()` for iterating over an array of multiple fields.
+ *
+ * @see lithium\template\helper\Form::field()
+ * @param array $fields An array of fields to render.
+ * @param array $options The array of options to apply to all fields in the `$fields` array. See
+ * the `$options` parameter of the `field` method for more information.
+ * @return string Returns the fields rendered by `field()`, each separated by a newline.
+ */
+ protected function _fields(array $fields, array $options = array()) {
+ $result = array();
- return $this->_render(__METHOD__, $template, $params);
+ foreach ($fields as $field => $label) {
+ if (is_numeric($field)) {
+ $field = $label;
+ unset($label);
+ }
+ $result[] = $this->field($field, compact('label') + $options);
+ }
+ return join("\n", $result);
}
/**
* Generates an HTML `<input type="submit" />` object.
*
* @param string $title The title of the submit button.
- * @param array $options
+ * @param array $options Any options passed are converted to HTML attributes within the
+ * `<input />` tag.
* @return string Returns a submit `<input />` tag with the given title and HTML attributes.
*/
public function submit($title = null, array $options = array()) {
@@ -293,10 +470,13 @@ class Form extends \lithium\template\Helper {
}
/**
- * Generates an HTML `<textarea></textarea>` object.
+ * Generates an HTML `<textarea>...</textarea>` object.
*
* @param string $name The name of the field.
- * @param array $options
+ * @param array $options The options to be used when generating the `<textarea />` tag pair,
+ * which are as follows:
+ * - `'value'` _string_: The content value of the field.
+ * - Any other options specified are rendered as HTML attributes of the element.
* @return string Returns a `<textarea>` tag with the given name and HTML attributes.
*/
public function textarea($name, array $options = array()) {
@@ -310,7 +490,7 @@ class Form extends \lithium\template\Helper {
* Generates an HTML `<input type="text" />` object.
*
* @param string $name The name of the field.
- * @param array $options
+ * @param array $options All options passed are rendered as HTML attributes.
* @return string Returns a `<input />` tag with the given name and HTML attributes.
*/
public function text($name, array $options = array()) {
@@ -370,28 +550,46 @@ class Form extends \lithium\template\Helper {
* Generates an HTML `<input type="checkbox" />` object.
*
* @param string $name The name of the field.
- * @param array $options
+ * @param array $options Options to be used when generating the checkbox `<input />` element:
+ * - `'checked'` _boolean_: Whether or not the field should be checked by default.
+ * - `'value'` _mixed_: if specified, it will be used as the 'value' html
+ * attribute and no hidden input field will be added
+ * - Any other options specified are rendered as HTML attributes of the element.
* @return string Returns a `<input />` tag with the given name and HTML attributes.
*/
public function checkbox($name, array $options = array()) {
+ $defaults = array('value' => '1', 'hidden' => true);
+ $options += $defaults;
+ $default = $options['value'];
+ $out = '';
+
list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
- list($scope, $options) = $this->_options(array('value' => null), $options);
+ list($scope, $options) = $this->_options($defaults, $options);
- if (!isset($scope['checked'])) {
- $options['checked'] = isset($scope['value']) ? $scope['value'] : false;
+ if (!isset($options['checked'])) {
+ if ($this->_binding && $bound = $this->_binding->data($name)) {
+ $options['checked'] = !($bound === $default);
+ } else {
+ $options['checked'] = ($scope['value'] != $default);
+ }
}
- return $this->_render(__METHOD__, $template, compact('name', 'options'));
+ if ($scope['hidden']) {
+ $out = $this->hidden($name, array('value' => '', 'id' => false));
+ }
+ $options['value'] = $scope['value'];
+ return $out . $this->_render(__METHOD__, $template, compact('name', 'options'));
}
/**
* Generates an HTML `<input type="password" />` object.
*
* @param string $name The name of the field.
- * @param array $options
+ * @param array $options An array of HTML attributes with which the field should be rendered.
* @return string Returns a `<input />` tag with the given name and HTML attributes.
*/
public function password($name, array $options = array()) {
list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
+ unset($options['value']);
return $this->_render(__METHOD__, $template, compact('name', 'options'));
}
@@ -399,7 +597,7 @@ class Form extends \lithium\template\Helper {
* Generates an HTML `<input type="hidden" />` object.
*
* @param string $name The name of the field.
- * @param array $options
+ * @param array $options An array of HTML attributes with which the field should be rendered.
* @return string Returns a `<input />` tag with the given name and HTML attributes.
*/
public function hidden($name, array $options = array()) {
@@ -410,19 +608,24 @@ class Form extends \lithium\template\Helper {
/**
* Generates an HTML `<label></label>` object.
*
- * @param string $name The name of the field that the label is for.
+ * @param string $name The DOM ID of the field that the label is for.
* @param string $title The content inside the `<label></label>` object.
- * @param array $options
+ * @param array $options Besides HTML attributes, this parameter allows one additional flag:
+ * - `'escape'` _boolean_: Defaults to `true`. Indicates whether the title of the
+ * label should be escaped. If `false`, it will be treated as raw HTML.
* @return string Returns a `<label>` tag for the name and with HTML attributes.
*/
public function label($name, $title = null, array $options = array()) {
$defaults = array('escape' => true);
+
+ if (is_array($title)) {
+ list($title, $options) = each($title);
+ }
$title = $title ?: Inflector::humanize($name);
+
list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
list($scope, $options) = $this->_options($defaults, $options);
- return $this->_render(
- __METHOD__, $template, compact('name', 'title', 'options'), $scope
- );
+ return $this->_render(__METHOD__, $template, compact('name', 'title', 'options'), $scope);
}
/**
@@ -431,29 +634,56 @@ class Form extends \lithium\template\Helper {
*
* @param string $name The name of the field for which to render an error.
* @param mixed $key If more than one error is present for `$name`, a key may be specified.
- * By default, the first available error is used.
+ * If `$key` is not set in the array of errors, or if `$key` is `true`, the first
+ * available error is used.
* @param array $options Any rendering options or HTML attributes to be used when rendering
* the error.
* @return string Returns a rendered error message based on the `'error'` string template.
*/
public function error($name, $key = null, array $options = array()) {
+ $defaults = array('class' => 'error');
list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
+ $options += $defaults;
+ $result = '';
+
+ if (isset($options['value'])) {
+ unset($options['value']);
+ }
if (!$this->_binding || !$content = $this->_binding->errors($name)) {
return null;
}
+
if (is_array($content)) {
- $content = !isset($content[$key]) ? reset($content) : $content[$key];
+ $errors = $content;
+
+ if ($key !== null) {
+ $content = !isset($errors[$key]) || $key === true ? reset($errors) : $errors[$key];
+ } else {
+ foreach ($errors as $content) {
+ $result .= $this->_render(__METHOD__, $template, compact('content', 'options'));
+ }
+ return $result;
+ }
}
return $this->_render(__METHOD__, $template, compact('content', 'options'));
}
+ /**
+ * Builds the defaults array for a method by name, according to the config.
+ *
+ * @param string $method The name of the method to create defaults for.
+ * @param string $name The `$name` supplied to the original method.
+ * @param string $options `$options` from the original method.
+ * @return array Defaults array contents.
+ */
protected function _defaults($method, $name, $options) {
$methodConfig = isset($this->_config[$method]) ? $this->_config[$method] : array();
$options += $methodConfig + $this->_config['base'];
+ $options = $this->_generators($method, $name, $options);
$hasValue = (
- (!isset($options['value']) || empty($options['value'])) &&
+ (!isset($options['value']) || $options['value'] === null) &&
$name && $this->_binding && $value = $this->_binding->data($name)
);
if ($hasValue) {
@@ -463,9 +693,46 @@ class Form extends \lithium\template\Helper {
$options['value'] = $options['default'];
}
unset($options['default']);
- $template = isset($this->_templateMap[$method]) ? $this->_templateMap[$method] : $method;
+
+ if (strpos($name, '.')) {
+ $name = explode('.', $name);
+ $first = array_shift($name);
+ $name = $first . '[' . join('][', $name) . ']';
+ }
+ $tplKey = isset($options['template']) ? $options['template'] : $method;
+ $template = isset($this->_templateMap[$tplKey]) ? $this->_templateMap[$tplKey] : $tplKey;
return array($name, $options, $template);
}
+
+ /**
+ * Iterates over the configured attribute generators, and modifies the settings for a tag.
+ *
+ * @param string $method The name of the helper method which was called, i.e. `'text'`,
+ * `'select'`, etc.
+ * @param string $name The name of the field whose attributes are being generated. Some helper
+ * methods, such as `create()` and `end()`, are not field-based, and therefore
+ * will have no name.
+ * @param array $options The options and HTML attributes that will be used to generate the
+ * helper output.
+ * @return array Returns the value of the `$options` array, modified by the attribute generators
+ * added in the `'attributes'` key of the helper's configuration. Note that if a
+ * generator is present for a field whose value is `false`, that field will be removed
+ * from the array.
+ */
+ protected function _generators($method, $name, $options) {
+ foreach ($this->_config['attributes'] as $key => $generator) {
+ if ($generator && !isset($options[$key])) {
+ if (($attr = $generator($method, $name, $options)) !== null) {
+ $options[$key] = $attr;
+ }
+ continue;
+ }
+ if ($generator && $options[$key] === false) {
+ unset($options[$key]);
+ }
+ }
+ return $options;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/template/helper/Html.php b/libraries/lithium/template/helper/Html.php
index 5071f8f..5350335 100644
--- a/libraries/lithium/template/helper/Html.php
+++ b/libraries/lithium/template/helper/Html.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -25,7 +25,7 @@ class Html extends \lithium\template\Helper {
'block' => '<div{:options}>{:content}</div>',
'block-end' => '</div>',
'block-start' => '<div{:options}>',
- 'charset' => '<meta http-equiv="Content-Type" content="{:type}; charset={:charset}" />',
+ 'charset' => '<meta charset="{:encoding}" />',
'image' => '<img src="{:path}"{:options} />',
'js-block' => '<script type="text/javascript"{:options}>{:content}</script>',
'js-end' => '</script>',
@@ -62,6 +62,14 @@ class Html extends \lithium\template\Helper {
);
/**
+ * List of meta tags to cache and to output.
+ *
+ * @var array
+ * @see lithium\template\helper\Html::meta()
+ */
+ protected $_metaList = array();
+
+ /**
* Used by output handlers to calculate asset paths in conjunction with the `Media` class.
*
* @var array
@@ -75,15 +83,35 @@ class Html extends \lithium\template\Helper {
);
/**
- * Returns a charset meta-tag.
+ * Returns a charset meta-tag for declaring the encoding of the document.
*
- * @param string $charset The character set to be used in the meta tag. Example: `"utf-8"`.
- * @return string A meta tag containing the specified character set.
+ * The terms character set (here: charset) and character encoding (here:
+ * encoding) were historically synonymous. The terms now have related but
+ * distinct meanings. Whenever possible Lithium tries to use precise
+ * terminology. Since HTML uses the term `charset` we expose this method
+ * under the exact same name. This caters to the expectation towards a HTML
+ * helper. However the rest of the framework will use the term `encoding`
+ * when talking about character encoding.
+ *
+ * It is suggested that uppercase letters should be used when specifying
+ * the encoding. HTML specs don't require it to be uppercase and sites in
+ * the wild most often use the lowercase variant. On the other hand must
+ * XML parsers (those may not be relevant in this context anyway) not
+ * support lowercase encodings. This and the fact that IANA lists only
+ * encodings with uppercase characters led to the above suggestion.
+ *
+ * @see lithium\net\http\Response::$encoding
+ * @link http://www.iana.org/assignments/character-sets
+ * @param string $encoding The character encoding to be used in the meta tag.
+ * Defaults to the encoding of the `Response` object attached to the
+ * current context. The default encoding of that object is `UTF-8`.
+ * The string given here is not manipulated in any way, so that
+ * values are rendered literally. Also see above note about casing.
+ * @return string A meta tag containing the specified encoding (literally).
*/
- public function charset($charset = null) {
- $options = array('type' => 'text/html');
- $options['charset'] = $charset ?: 'utf-8';
- return $this->_render(__METHOD__, 'charset', $options);
+ public function charset($encoding = null) {
+ $encoding = $encoding ?: $this->_context->response()->encoding;
+ return $this->_render(__METHOD__, 'charset', compact('encoding'));
}
/**
@@ -188,6 +216,34 @@ class Html extends \lithium\template\Helper {
}
/**
+ * Creates a tag for the ```<head>``` section of your document.
+ *
+ * If there is a rendering context, then it also pushes the resulting tag to it.
+ *
+ * The ```$options``` must match the named parameters from ```$_strings``` for the
+ * given ```$tag```.
+ *
+ * @param string $tag the name of a key in ```$_strings```
+ * @param array $options the options required by ```$_strings[$tag]```
+ * @return mixed a string if successful, otherwise NULL
+ * @filter This method can be filtered.
+ */
+ public function head($tag, array $options) {
+ if(!isset($this->_strings[$tag])) {
+ return NULL;
+ }
+ $method = __METHOD__;
+ $filter = function($self, $options, $chain) use ($method, $tag) {
+ return $self->invokeMethod('_render', array($method, $tag, $options));
+ };
+ $head = $this->_filter($method, $options, $filter);
+ if($this->_context) {
+ $this->_context->head($head);
+ }
+ return $head;
+ }
+
+ /**
* Creates a formatted <img /> element.
*
* @param string $path Path to the image file, relative to the app/webroot/img/ directory.
diff --git a/libraries/lithium/template/readme.wiki b/libraries/lithium/template/readme.wiki
index 5919dd1..3b8ab6c 100644
--- a/libraries/lithium/template/readme.wiki
+++ b/libraries/lithium/template/readme.wiki
@@ -1,3 +1,4 @@
+
#### Special syntax
Views have a special syntax for outputting escaped text. The standard way to
@@ -5,11 +6,14 @@ output escaped text in your views from Lithium is as follows: {{{
<?=$variable;?>
}}}
-This is where a lot of confusion comes in because it is commonly misunderstood
+This is where a lot of confusion comes in, because it is commonly misunderstood
that Lithium depends on `short_open_tags`, however, that's not the case. The
-contents of a view is processed through a [ stream](template/view/Stream) before
-it is included by PHP. See the PHP [ manual](http://www.php.net/streams) to learn
-more about streams.
+contents of a view are processed through a [ tokenizer](template/view/Compier) before
+it is included by PHP. The file is then `compiled` into the final PHP+HTML (or whatever
+other content type that is requsted), which is then passed off to be fully rendered
+by the two-step view to its final form.
+
+See the PHP [ manual](http://php.net/manual/en/book.tokenizer.php) to learn more about tokens.
The stream wrapper reads the file and searches for anything that looks like
`<?=...?>` and replaces it with `<?php echo $h(...); ?>`.
@@ -37,8 +41,8 @@ An example would be something like: {{{
**Other useful information:**
-- [Introduction to PHP streams](http://www.php.net/intro.stream)
-- [Stream examples](http://www.php.net/stream.examples)
+ - [Introduction to PHP streams](http://www.php.net/intro.stream)
+ - [Stream examples](http://www.php.net/stream.examples)
#### Using helpers
@@ -53,4 +57,87 @@ it so that the next time the helper is invoked the renderer will not have to
re-instanciate the helper.
Using such an approach, helpers can easily be loaded as needed without any
-performance impact.
\ No newline at end of file
+performance impact.
+
+**More info**
+
+ - [ HTML helper](template/helper/Html)
+ - [ Form helper](template/helper/Form)
+ - [ Helper base class](template/Helper)
+
+#### Creating custom helpers
+
+You can also create your own custom helper very easily by extending the `Helper` base class, and
+placing your helper in the correct namespace. By default, helpers belong in the
+`<library>\extensions\helper` namespace, but this can be changed through configuration (see the
+[the `Libraries` class](core/Libraries)). For example, consider the following class, saved as
+`app/extensions/helper/Custom.php`:
+{{{
+<?php
+
+namespace app\extensions\helper;
+
+class Custom extends \lithium\template\Helper {
+
+ public function greeting($name) {
+ return "Hello {$name}!";
+ }
+}
+
+?>
+}}}
+
+You can then use your helper in templates as follows:
+{{{
+<?=$this->custom->greeting("World"); ?>
+}}}
+
+Your custom helper will then be auto-loaded into the templating engine from your application or a
+plugin.
+
+#### Extending core helpers
+
+Because your application and plugins have a higher order-of-precedence than the Lithium core,
+classes like helpers can be extended and replaced seamlessly, without any changes to your templates.
+
+For example, to add or replace methods in the `Form` helper, you can add the following to
+`app/extensions/helper/Form.php`:
+
+{{{
+<?php
+
+namespace app\extensions\helper;
+
+class Form extends \lithium\template\helper\Form {
+
+ // Add or override Form helper methods
+}
+
+?>
+}}}
+
+Your custom `Form` helper will now be invoked in all instances where `$this->form` is called in a
+template. For more information on the load order of classes, see
+[the `locate()` method of the `Libraries` class](core/Libraries::locate)
+
+#### Rendering elements
+
+Elements are reusable view snippets that you can use in several views and layouts.
+You can reference it like so:
+{{{
+echo $this->view()->render(
+ array('element' => 'menu'),
+ array('var1' => $var1, 'var2' => $var2)
+);
+}}}
+
+Where `menu` is the name of your element file, in this example `app/views/elements/menu.html.php`
+and `var1` and `var2` are variables either declared in the view or set in the controller (as
+per the normal way of passing variables to views with `return` or `$this->set`) that you want the
+element to be able to access. For short syntax you can use `compact('var1','var2')`.
+
+**More info**
+
+ - [ View](template/View)
+ - [ Renderer](template/view/Renderer)
+ - [ File adapter](template/view/adapter/File)
\ No newline at end of file
diff --git a/libraries/lithium/template/view/Compiler.php b/libraries/lithium/template/view/Compiler.php
index f160fa1..add24a5 100644
--- a/libraries/lithium/template/view/Compiler.php
+++ b/libraries/lithium/template/view/Compiler.php
@@ -2,38 +2,57 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\template\view;
-use \Exception;
+use lithium\core\Libraries;
+use lithium\template\TemplateException;
/**
* The template compiler is a simple string replacement engine which allows PHP templates to be
* overridden with custom syntax. The default process rules allow PHP templates using short-echo
* syntax (`<?=`) to be rewritten to full PHP tags which automatically escape their output.
+ *
+ * It is possible to create your own template compiler and have the chosen `View` adapter use that
+ * instead. Please see the documentation on the dynamic dependencies of the adapter in question
+ * to know more about how this can be achieved.
+ *
+ * @see lithium\template\View
+ * @see lithium\template\view\adapter
*/
class Compiler extends \lithium\core\StaticObject {
/**
* The list of syntax replacements to apply to compiled templates.
+ *
+ * Key/value pairs of regular expressions. The keys are the regexes, and the values are the
+ * resulting expressions along with any capture groups that may have been used in the
+ * corresponding regexes.
+ *
+ * @var array
*/
protected static $_processors = array(
'/\<\?=\s*\$this->(.+?)\s*;?\s*\?>/msx' => '<?php echo $this->$1; ?>',
+ '/\<\?=\s*(\$h\(.+?)\s*;?\s*\?>/msx' => '<?php echo $1; ?>',
'/\<\?=\s*(.+?)\s*;?\s*\?>/msx' => '<?php echo $h($1); ?>'
);
/**
* Compiles a template and writes it to a cache file, which is used for inclusion.
*
- * @param string $file The full
- * @param string $options
- * @return void
+ * @param string $file The full path to the template that will be compiled.
+ * @param string $options Options for compilation include:
+ * - `path`: Path where the compiled template should be written.
+ * - `fallback`: Boolean indicating that if the compilation failed for some
+ * reason (e.g. `path` is not writable), that the compiled template
+ * should still be returned and no exception be thrown.
+ * @return string The compiled template.
*/
public static function template($file, array $options = array()) {
- $cachePath = LITHIUM_APP_PATH . '/resources/tmp/cache/templates';
+ $cachePath = Libraries::get(true, 'resources') . '/tmp/cache/templates';
$defaults = array('path' => $cachePath, 'fallback' => true);
$options += $defaults;
@@ -59,9 +78,17 @@ class Compiler extends \lithium\core\StaticObject {
if ($options['fallback']) {
return $file;
}
- throw new Exception("Could not write compiled template {$template} to cache");
+ throw new TemplateException("Could not write compiled template `{$template}` to cache.");
}
+ /**
+ * Preprocess the passed `$string` (usually a PHP template) for syntax replacements
+ * using sets of regular expressions.
+ *
+ * @see lithium\template\view\Compiler::$_processors
+ * @param string $string The string to be preprocessed.
+ * @return string Processed string.
+ */
public static function compile($string) {
$patterns = static::$_processors;
return preg_replace(array_keys($patterns), array_values($patterns), $string);
diff --git a/libraries/lithium/template/view/Renderer.php b/libraries/lithium/template/view/Renderer.php
index 48ea460..4e76679 100644
--- a/libraries/lithium/template/view/Renderer.php
+++ b/libraries/lithium/template/view/Renderer.php
@@ -2,15 +2,29 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\template\view;
-use \RuntimeException;
-use \lithium\core\Libraries;
+use RuntimeException;
+use lithium\core\Libraries;
+use lithium\core\ClassNotFoundException;
+/**
+ * The `Renderer` abstract class serves as a base for all concrete `Renderer` adapters.
+ *
+ * When in a view, the local scope is that of an instance of `Renderer` - meaning that
+ * `$this` in views is an instance of the current renderer adapter.
+ *
+ * For more information about implementing your own template loaders or renderers, see the
+ * `lithium\template\View` class.
+ *
+ * @see lithium\template\View
+ * @see lithium\template\adapter\File
+ * @see lithium\template\adapter\Simple
+ */
abstract class Renderer extends \lithium\core\Object {
/**
@@ -20,7 +34,7 @@ abstract class Renderer extends \lithium\core\Object {
* @var array
*/
protected $_autoConfig = array(
- 'request', 'context', 'strings', 'handlers', 'view', 'classes' => 'merge'
+ 'request', 'response', 'context', 'strings', 'handlers', 'view', 'classes' => 'merge'
);
/**
@@ -39,7 +53,7 @@ abstract class Renderer extends \lithium\core\Object {
* @var array
*/
protected $_context = array(
- 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array()
+ 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array(), 'head' => array()
);
/**
@@ -71,9 +85,21 @@ abstract class Renderer extends \lithium\core\Object {
*/
protected $_strings = array();
+ /**
+ * The `Request` object instance, if applicable.
+ *
+ * @var object The request object.
+ */
protected $_request = null;
/**
+ * The `Response` object instance, if applicable.
+ *
+ * @var object The response object.
+ */
+ protected $_response = null;
+
+ /**
* Automatically matches up template strings by name to output handlers. A handler can either
* be a string, which represents a method name of the helper, or it can be a closure or callable
* object. A handler takes 3 parameters: the value to be filtered, the name of the helper
@@ -99,6 +125,14 @@ abstract class Renderer extends \lithium\core\Object {
protected $_data = array();
/**
+ * Variables that have been set from a view/element/layout/etc. that should be available to the
+ * same rendering context.
+ *
+ * @var array Key/value pairs of variables
+ */
+ protected $_vars = array();
+
+ /**
* Render the template with given data.
* Abstract. Must be added to subclasses.
*
@@ -110,9 +144,20 @@ abstract class Renderer extends \lithium\core\Object {
abstract public function render($template, $data = array(), array $options = array());
/**
- * undocumented function
+ * Renderer constructor.
+ *
+ * Accepts these following configuration parameters:
+ * - `view`: The `View` object associated with this renderer.
+ * - `strings`: String templates used by helpers.
+ * - `handlers`: An array of output handlers for string template inputs.
+ * - `request`: The `Request` object associated with this renderer and passed to the
+ * defined handlers.
+ * - `response`: The `Response` object associated with this renderer.
+ * - `context`: An array of the current rendering context data, including `content`,
+ * `title`, `scripts`, `head` and `styles`.
*
* @param array $config
+ * @return void
*/
public function __construct(array $config = array()) {
$defaults = array(
@@ -120,8 +165,10 @@ abstract class Renderer extends \lithium\core\Object {
'strings' => array(),
'handlers' => array(),
'request' => null,
+ 'response' => null,
'context' => array(
- 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array()
+ 'content' => '', 'title' => '', 'scripts' => array(),
+ 'styles' => array(), 'head' => array()
)
);
parent::__construct((array) $config + $defaults);
@@ -140,30 +187,45 @@ abstract class Renderer extends \lithium\core\Object {
$classes =& $this->_classes;
$this->_handlers += array(
- 'url' => function($url) use (&$classes, &$request) {
- return $classes['router']::match($url ?: '', $request);
+ 'url' => function($url, $ref, array $options = array()) use (&$classes, &$request) {
+ return $classes['router']::match($url ?: '', $request, $options);
},
'path' => function($path, $ref, array $options = array()) use (&$classes, &$request) {
$defaults = array('base' => $request ? $request->env('base') : '');
- list($helper, $methodRef) = $ref;
- list($class, $method) = explode('::', $methodRef);
- $type = $helper->contentMap[$method];
+ $type = 'generic';
+
+ if (is_array($ref) && $ref[0] && $ref[1]) {
+ list($helper, $methodRef) = $ref;
+ list($class, $method) = explode('::', $methodRef);
+ $type = $helper->contentMap[$method];
+ }
return $classes['media']::asset($path, $type, $options + $defaults);
},
'options' => '_attributes',
- 'content' => 'escape',
'title' => 'escape',
'scripts' => function($scripts) use (&$context) {
return "\n\t" . join("\n\t", $context['scripts']) . "\n";
},
'styles' => function($styles) use (&$context) {
return "\n\t" . join("\n\t", $context['styles']) . "\n";
+ },
+ 'head' => function($head) use (&$context) {
+ return "\n\t" . join("\n\t", $context['head']) . "\n";
}
);
unset($this->_config['view']);
}
- public function __isSet($property) {
+ /**
+ * Magic `__isset` method.
+ *
+ * Is triggered by calling isset() or empty() on inaccessible properties, and performs
+ * an `isset()` check on for keys in the current `context`.
+ *
+ * @param string $property The accessed property.
+ * @return boolean True if set, false otherwise.
+ */
+ public function __isset($property) {
return isset($this->_context[$property]);
}
@@ -203,10 +265,10 @@ abstract class Renderer extends \lithium\core\Object {
if (!isset($this->_context[$method]) && !isset($this->_handlers[$method])) {
return isset($params[0]) ? $params[0] : null;
}
- if (!isset($this->_handlers[$method]) && empty($params)) {
+ if (!isset($this->_handlers[$method]) && !$params) {
return $this->_context[$method];
}
- if (isset($this->_context[$method]) && !empty($params)) {
+ if (isset($this->_context[$method]) && $params) {
if (is_array($this->_context[$method])) {
$this->_context[$method][] = $params[0];
} else {
@@ -228,11 +290,16 @@ abstract class Renderer extends \lithium\core\Object {
* @param array $config
* @return object
*/
- public function helper($name, $config = array()) {
- if ($class = Libraries::locate('helper', ucfirst($name))) {
- return $this->_helpers[$name] = new $class($config + array('context' => $this));
+ public function helper($name, array $config = array()) {
+ if (isset($this->_helpers[$name])) {
+ return $this->_helpers[$name];
+ }
+ try {
+ $config += array('context' => $this);
+ return $this->_helpers[$name] = Libraries::instance('helper', ucfirst($name), $config);
+ } catch (ClassNotFoundException $e) {
+ throw new RuntimeException("Helper `{$name}` not found.");
}
- throw new RuntimeException("Helper {$name} not found");
}
/**
@@ -263,7 +330,7 @@ abstract class Renderer extends \lithium\core\Object {
* @return mixed A string or array, depending on whether `$property` is specified.
*/
public function context($property = null) {
- if (!empty($property)) {
+ if ($property) {
return isset($this->_context[$property]) ? $this->_context[$property] : null;
}
return $this->_context;
@@ -321,7 +388,10 @@ abstract class Renderer extends \lithium\core\Object {
if (!(isset($this->_handlers[$name]) && $handler = $this->_handlers[$name])) {
return $value;
}
+
switch (true) {
+ case is_string($handler) && !$helper:
+ $helper = $this->helper('html');
case is_string($handler) && is_object($helper):
return $helper->invokeMethod($handler, array($value, $method, $options));
case is_array($handler) && is_object($handler[0]):
@@ -345,6 +415,16 @@ abstract class Renderer extends \lithium\core\Object {
}
/**
+ * Returns the `Response` object associated with this rendering context.
+ *
+ * @return object Returns an instance of `lithium\action\Response`, which provides the i.e.
+ * the encoding for the document being the result of templates rendered by this context.
+ */
+ public function response() {
+ return $this->_response;
+ }
+
+ /**
* Retuns the `View` object that controls this rendering context's instance. This can be used,
* for example, to render view elements, i.e. `<?=$this->view()->render('element' $name); ?>`.
*
@@ -355,6 +435,15 @@ abstract class Renderer extends \lithium\core\Object {
}
/**
+ * Returns all variables and their values that have been set.
+ *
+ * @return array Key/value pairs of data that has been set.
+ */
+ public function data() {
+ return $this->_data + $this->_vars;
+ }
+
+ /**
* Allows variables to be set by one template and used in subsequent templates rendered using
* the same context. For example, a variable can be set in a template and used in an element
* rendered within a template, or an element or template could set a variable which would be
@@ -364,8 +453,31 @@ abstract class Renderer extends \lithium\core\Object {
* made available to all other templates rendered in this rendering context.
* @return void
*/
- public function set($data = array()) {
+ public function set(array $data = array()) {
$this->_data = $data + $this->_data;
+ $this->_vars = $data + $this->_vars;
+ }
+
+ /**
+ * Shortcut method used to render elements and other nested templates from inside the templating
+ * layer.
+ *
+ * @see lithium\template\View::$_processes
+ * @see lithium\template\View::render()
+ * @param string $type The type of template to render, usually either `'element'` or
+ * `'template'`. Indicates the process used to render the content. See
+ * `lithium\template\View::$_processes` for more info.
+ * @param string $template The template file name. For example, if `'header'` is passed, and
+ * `$type` is set to `'element'`, then the template rendered will be
+ * `views/elements/header.html.php` (assuming the default configuration).
+ * @param array $data An array of any other local variables that should be injected into the
+ * template. By default, only the values used to render the current template will
+ * be sent. If `$data` is non-empty, both sets of variables will be merged.
+ * @param array $options Any options accepted by `template\View::render()`.
+ * @return string Returns a the rendered template content as a string.
+ */
+ protected function _render($type, $template, array $data = array(), array $options = array()) {
+ return $this->_view->render($type, $data + $this->_data, compact('template') + $options);
}
}
diff --git a/libraries/lithium/template/view/adapter/File.php b/libraries/lithium/template/view/adapter/File.php
index b05f284..ee5fede 100644
--- a/libraries/lithium/template/view/adapter/File.php
+++ b/libraries/lithium/template/view/adapter/File.php
@@ -2,19 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\template\view\adapter;
-use \Exception;
-use \lithium\util\String;
-use \lithium\core\Libraries;
+use lithium\util\String;
+use lithium\core\Libraries;
+use lithium\template\TemplateException;
/**
- * The File adapter implements both template loading and rendering, and uses the `view\Stream` class
- * to auto-escape template output with short tags (i.e. <?=).
+ * The File adapter implements both template loading and rendering, and uses the
+ * `lithium\template\view\Stream` class or `lithium\template\view\Compiler` class to auto-escape
+ * template output with short tags (i.e. `<?=`).
*
* For more information about implementing your own template loaders or renderers, see the
* `lithium\template\View` class.
@@ -22,10 +23,17 @@ use \lithium\core\Libraries;
* @see lithium\template\View
* @see lithium\template\view\Compiler
*/
-class File extends \lithium\template\view\Renderer {
+class File extends \lithium\template\view\Renderer implements \ArrayAccess {
+ /**
+ * These configuration variables will automatically be assigned to their corresponding protected
+ * properties when the object is initialized.
+ *
+ * @var array
+ */
protected $_autoConfig = array(
- 'classes' => 'merge', 'request', 'context', 'strings', 'handlers', 'view', 'compile'
+ 'classes' => 'merge', 'request', 'response', 'context',
+ 'strings', 'handlers', 'view', 'compile', 'paths'
);
/**
@@ -37,15 +45,43 @@ class File extends \lithium\template\view\Renderer {
*/
protected $_compile = true;
+ /**
+ * An array containing the variables currently in the scope of the template. These values are
+ * manipulable using array syntax against the template object, i.e. `$this['foo'] = 'bar'`
+ * inside your template files.
+ *
+ * @var array
+ */
+ protected $_data = array();
+
+ /**
+ * Variables that have been set from a view/element/layout/etc. that should be available to the
+ * same rendering context.
+ *
+ * @var array Key/value pairs of variables
+ */
+ protected $_vars = array();
+
+ protected $_paths = array();
+
+ /**
+ * `File`'s dependencies. These classes are used by the output handlers to generate URLs
+ * for dynamic resources and static assets, as well as compiling the templates.
+ *
+ * @see Renderer::$_handlers
+ * @var array
+ */
protected $_classes = array(
- 'compiler' => '\lithium\template\view\Compiler',
+ 'compiler' => 'lithium\template\view\Compiler',
'router' => 'lithium\net\http\Router',
'media' => 'lithium\net\http\Media'
);
public function __construct(array $config = array()) {
- $defaults = array('classes' => array(), 'compile' => true);
- parent::__construct((array) $config + $defaults);
+ $defaults = array(
+ 'classes' => array(), 'compile' => true, 'extract' => true, 'paths' => array()
+ );
+ parent::__construct($config + $defaults);
}
/**
@@ -57,10 +93,19 @@ class File extends \lithium\template\view\Renderer {
* @return string
*/
public function render($template, $data = array(), array $options = array()) {
+ $defaults = array('context' => array());
+ $options += $defaults;
+
$this->_context = $options['context'] + $this->_context;
+ $this->_data = (array) $data + $this->_vars;
$template__ = $template;
- unset($options, $template);
- extract($data, EXTR_OVERWRITE);
+ unset($options, $template, $defaults, $data);
+
+ if ($this->_config['extract']) {
+ extract($this->_data, EXTR_OVERWRITE);
+ } elseif ($this->_view) {
+ extract((array) $this->_view->outputFilters, EXTR_OVERWRITE);
+ }
ob_start();
include $template__;
@@ -71,22 +116,13 @@ class File extends \lithium\template\view\Renderer {
* Returns a template file name
*
* @param string $type
- * @param array $options
+ * @param array $params
* @return string
*/
- public function template($type, $options) {
- if (!isset($this->_config['paths'][$type])) {
- return null;
- }
- $options = array_filter($options, function($item) { return is_string($item); });
-
- if (isset($options['plugin'])) {
- $options['library'] = $options['plugin'];
- }
-
- $library = Libraries::get(isset($options['library']) ? $options['library'] : true);
- $options['library'] = $library['path'];
- $path = $this->_paths((array) $this->_config['paths'][$type], $options);
+ public function template($type, array $params) {
+ $library = Libraries::get(isset($params['library']) ? $params['library'] : true);
+ $params['library'] = $library['path'];
+ $path = $this->_paths($type, $params);
if ($this->_compile) {
$compiler = $this->_classes['compiler'];
@@ -96,21 +132,48 @@ class File extends \lithium\template\view\Renderer {
}
/**
- * Searches a series of path templates for a matching template file, and returns the file name.
+ * Allows checking to see if a value is set in template data, i.e. `$this['foo']` in templates.
+ *
+ * @param string $offset The key / variable name to check.
+ * @return boolean Returns `true` if the value is set, otherwise `false`.
+ */
+ public function offsetExists($offset) {
+ return array_key_exists($offset, $this->_data);
+ }
+
+ public function offsetGet($offset) {
+ return isset($this->_data[$offset]) ? $this->_data[$offset] : null;
+ }
+
+ public function offsetSet($offset, $value) {
+ $this->_data[$offset] = $value;
+ }
+
+ public function offsetUnset($offset) {
+ unset($this->_data[$offset]);
+ }
+
+ /**
+ * Searches one or more path templates for a matching template file, and returns the file name.
*
- * @param array $paths The array of path templates to search.
- * @param array $options The set of options keys to be interpolated into the path templates
+ * @param string $type
+ * @param array $params The set of options keys to be interpolated into the path templates
* when searching for the correct file to load.
* @return string Returns the first template file found. Throws an exception if no templates
* are available.
*/
- protected function _paths($paths, $options) {
- foreach ($paths as $path) {
- if (file_exists($path = String::insert($path, $options))) {
- return $path;
+ protected function _paths($type, array $params) {
+ if (!isset($this->_paths[$type])) {
+ throw new TemplateException("Invalid template type '{$type}'.");
+ }
+
+ foreach ((array) $this->_paths[$type] as $path) {
+ if (!file_exists($path = String::insert($path, $params))) {
+ continue;
}
+ return $path;
}
- throw new Exception("Template not found at {$path}");
+ throw new TemplateException("Template not found at path `{$path}`.");
}
}
diff --git a/libraries/lithium/template/view/adapter/Simple.php b/libraries/lithium/template/view/adapter/Simple.php
index 5f456c3..703f790 100644
--- a/libraries/lithium/template/view/adapter/Simple.php
+++ b/libraries/lithium/template/view/adapter/Simple.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\template\view\adapter;
-use \Closure;
-use \Exception;
-use \lithium\util\Set;
-use \lithium\util\String;
+use Closure;
+use Exception;
+use lithium\util\Set;
+use lithium\util\String;
/**
* This view adapter renders content using simple string substitution, and is only useful for very
@@ -50,7 +50,10 @@ class Simple extends \lithium\template\view\Renderer {
* @return string
*/
public function template($type, $options) {
- return isset($options[$type]) ? $options[$type] : '';
+ if (isset($options[$type])) {
+ return $options[$type];
+ }
+ return isset($options['template']) ? $options['template'] : '';
}
protected function _toString($data) {
diff --git a/libraries/lithium/test/Controller.php b/libraries/lithium/test/Controller.php
index 69a1a7c..ab2983c 100644
--- a/libraries/lithium/test/Controller.php
+++ b/libraries/lithium/test/Controller.php
@@ -2,83 +2,60 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test;
-use \lithium\test\Dispatcher;
-use \lithium\core\Libraries;
+use lithium\test\Dispatcher;
+use lithium\core\Libraries;
+use lithium\test\Group;
/**
- * Controller for reporting test results in html
+ * The Test Controller for running the html version of the test suite
*
*/
class Controller extends \lithium\core\Object {
/**
- * Invoke the `_data()` and `_render()` methods inside of a method filter
+ * Magic method to make Controller callable.
*
- * @param string $request
- * @param string $params
- * @param string $options
- * @return void
+ * @see lithium\action\Dispatcher::_callable()
+ * @param object $request A \lithium\action\Request object.
+ * @param array $dispatchParams Array of params after being parsed by router.
+ * @param array $options Some basic options for this controller.
+ * @return string
*/
- public function __invoke($request, $params, array $options = array()) {
- error_reporting(E_ALL | E_STRICT | E_DEPRECATED);
- $filter = function($self, $params, $chain) {
- try {
- $result = $self->invokeMethod('_data', $params);
- return $self->invokeMethod('_render', array('layout', $result));
- } catch (Exception $e) {
- throw $e;
- }
- };
- echo $this->_filter(__METHOD__, array($request, $params, $options), $filter);
- }
+ public function __invoke($request, $dispatchParams, array $options = array()) {
+ $dispatchParamsDefaults = array('args' => array());
+ $dispatchParams += $dispatchParamsDefaults;
+ $defaults = array('reporter' => 'html', 'format' => 'html', 'timeout' => 0);
+ $options += (array) $request->query + $defaults;
+ $params = compact('request', 'dispatchParams', 'options');
- /**
- * Base method for gathering data
- *
- * @param string $request
- * @param string $params
- * @param string $options
- * @return array request, group, report, filters, classes, menu
- */
- protected function _data($request, $params, $options) {
- $group = '\\' . join('\\', $request->args);
- $report = Dispatcher::run($group , $request->query + array(
- 'reporter' => 'html'
- ));
- $filters = Libraries::locate('test.filter', null, array(
- 'exclude' => '/Base$/'
- ));
- $classes = Libraries::locate('tests', null, array(
- 'filter' => '/cases|integration|functional/',
- 'exclude' => '/mocks/'
- ));
- $menu = $report->reporter->menu($classes, array(
- 'request' => $request, 'tree' => true
- ));
- return compact('request', 'group', 'report', 'filters', 'classes', 'menu');
- }
+ return $this->_filter(__METHOD__, $params, function($self, $params) {
+ $request = $params['request'];
+ $options = $params['options'];
+ $params = $params['dispatchParams'];
+ set_time_limit((integer) $options['timeout']);
+ $group = join('\\', (array) $params['args']);
- /**
- * Grab a the `layout.html.php` template and return output
- *
- * @param string $template name of the template (eg: layout)
- * @param string $data array from `_data()` method
- * @return string
- */
- protected function _render($template, $data) {
- $template = Libraries::locate('test.reporter.template', $template, array(
- 'filter' => false, 'type' => 'file', 'suffix' => '.html.php',
- ));
- extract($data);
- ob_start();
- include $template;
- return ob_get_clean();
+ if ($group === "all") {
+ $group = Group::all();
+ $options['title'] = 'All Tests';
+ }
+ $report = Dispatcher::run($group, $options);
+ $filters = Libraries::locate('test.filter');
+ $menu = Libraries::locate('tests', null, array(
+ 'filter' => '/cases|integration|functional/',
+ 'exclude' => '/mocks/'
+ ));
+ sort($menu);
+
+ $result = compact('request', 'report', 'filters', 'menu');
+ return $report->render('layout', $result);
+ });
}
}
diff --git a/libraries/lithium/test/Dispatcher.php b/libraries/lithium/test/Dispatcher.php
index 6e6a892..d98346b 100644
--- a/libraries/lithium/test/Dispatcher.php
+++ b/libraries/lithium/test/Dispatcher.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test;
-use \lithium\util\Set;
-use \lithium\util\Inflector;
-use \lithium\core\Libraries;
+use lithium\util\Set;
+use lithium\util\Inflector;
+use lithium\core\Libraries;
+use lithium\core\Environment;
/**
* The Lithium Test Dispatcher
@@ -26,8 +27,8 @@ class Dispatcher extends \lithium\core\StaticObject {
* @var array Key/value array of short identifier for the fully-namespaced class.
*/
protected static $_classes = array(
- 'group' => '\lithium\test\Group',
- 'report' => '\lithium\test\Report'
+ 'group' => 'lithium\test\Group',
+ 'report' => 'lithium\test\Report'
);
/**
@@ -48,33 +49,37 @@ class Dispatcher extends \lithium\core\StaticObject {
$defaults = array(
'title' => $group,
'filters' => array(),
- 'reporter' => 'text'
+ 'reporter' => 'text',
);
$options += $defaults;
+ $isCase = is_string($group) && preg_match('/Test$/', $group);
+ $items = ($isCase) ? array(new $group()) : (array) $group;
- $items = (array) $group;
- $isCase = preg_match('/Test$/', $group);
-
- if ($isCase) {
- $items = array(new $group());
- }
$options['filters'] = Set::normalize($options['filters']);
$group = static::_group($items);
$report = static::_report($group, $options);
- $report->run();
- return $report;
+
+ return static::_filter(__FUNCTION__, compact('report'), function($self, $params, $chain) {
+ $environment = Environment::get();
+ Environment::set('test');
+
+ $params['report']->run();
+
+ Environment::set($environment);
+ return $params['report'];
+ });
}
/**
* Creates the group class based
*
- * @param array $items array of cases or groups
- * @return object Group object constructed with $items
- * @see \lithium\test\Dispatcher::$_classes
+ * @see lithium\test\Dispatcher::$_classes
+ * @param array $data Array of cases or groups.
+ * @return object Group object constructed with `$data`.
*/
- protected static function _group($items) {
+ protected static function _group($data) {
$group = Libraries::locate('test', static::$_classes['group']);
- $class = new $group(compact('items'));
+ $class = static::_instance($group, compact('data'));
return $class;
}
@@ -82,15 +87,15 @@ class Dispatcher extends \lithium\core\StaticObject {
* Creates the test report class based on either the passed test case or the
* passed test group.
*
+ * @see lithium\test\Dispatcher::$_classes
* @param string $group
* @param array $options Options array passed from Dispatcher::run(). Should contain
* one of 'case' or 'group' keys.
* @return object Group object constructed with the test case or group passed in $options.
- * @see \lithium\test\Dispatcher::$_classes
*/
protected static function _report($group, $options) {
$report = Libraries::locate('test', static::$_classes['report']);
- $class = new $report(compact('group') + $options);
+ $class = static::_instance($report, compact('group') + $options);
return $class;
}
}
diff --git a/libraries/lithium/test/Filter.php b/libraries/lithium/test/Filter.php
index 6ca704e..9ef30f9 100644
--- a/libraries/lithium/test/Filter.php
+++ b/libraries/lithium/test/Filter.php
@@ -2,35 +2,38 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test;
/**
- * `Base` is the base class for all test filters.
+ * `Filter` is the base class for all test filters.
*/
-class Filter extends \lithium\core\StaticObject {
+abstract class Filter extends \lithium\core\StaticObject {
/**
* Takes an instance of an object (usually a Collection object) containing test
* instances. Allows for preparing tests before they are run.
*
* @param object $report Instance of Report which is calling apply.
+ * @param array $tests The test to apply this filter on
* @param array $options Options for how this filter should be applied.
* @return object|void Returns the instance of `$tests`.
*/
- public static function apply($report, array $options = array()) {}
+ public static function apply($report, $tests, array $options = array()) {}
/**
* Analyzes the results of a test run and returns the result of the analysis.
*
* @param object $report The report instance running this filter and aggregating results
* @param array $options
- * @return array|void The results of the analysis.
+ * @return array The results of the analysis.
*/
- public static function analyze($report, array $options = array()) {}
+ public static function analyze($report, array $options = array()) {
+ return $report->results['filters'][get_called_class()];
+ }
/**
* Returns data to be output by a reporter.
diff --git a/libraries/lithium/test/Group.php b/libraries/lithium/test/Group.php
index fc56cbf..0cfaca6 100644
--- a/libraries/lithium/test/Group.php
+++ b/libraries/lithium/test/Group.php
@@ -2,19 +2,35 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test;
-use \lithium\core\Libraries;
-use \lithium\util\Collection;
+use Exception;
+use lithium\test\Unit;
+use lithium\core\Libraries;
+use lithium\util\Collection;
/**
- * Group Test Collection
+ * A `Collection` of tests that represents a test group.
*
- * @package lithium.test
+ * Tests are added to this group either on `construct` by passing a fully-namespaced test class
+ * or namespace string-based path, e.g.
+ *
+ * {{{
+ * $group = new Group(array('data' => array(
+ * 'data\ModelTest',
+ * new \lithium\tests\cases\core\ObjectTest()
+ * )));
+ * }}}
+ *
+ * Or they can be added programmatically:
+ *
+ * {{{
+ * $group->add('data\ModelTest');
+ * }}}
*/
class Group extends \lithium\util\Collection {
@@ -25,71 +41,56 @@ class Group extends \lithium\util\Collection {
*/
protected function _init() {
parent::_init();
- $items = $this->_items;
- $this->_items = array();
- foreach ($items as $item) {
+ $data = $this->_data;
+ $this->_data = array();
+
+ foreach ($data as $item) {
$this->add($item);
}
}
/**
- * Get all tests
+ * Get all test cases. By default, does not include function or integration tests.
*
* @param string $options
* @return array
*/
public static function all(array $options = array()) {
- $defaults = array('transform' => false, 'library' => true);
- $options += $defaults;
- $m = '/\\\\tests\\\\cases\\\\(.+)Test$/';
- $transform = function($class) use ($m) { return preg_replace($m, '\\\\\1', $class); };
- $classes = Libraries::locate('tests', null, $options + array(
- 'filter' => '/cases|integration|functional/', 'recursive' => true
- ));
- return $options['transform'] ? array_map($transform, $classes) : $classes;
+ $defaults = array(
+ 'filter' => '/cases/',
+ 'exclude' => '/mock/',
+ 'recursive' => true,
+ );
+ return Libraries::locate('tests', null, $options + $defaults);
}
/**
- * Add a tests to the group
+ * Add a tests to the group.
*
- * @param string $test
- * @param string $options
- * @return array
+ * @param string $test The test to be added.
+ * @param string $options Method options. Currently not used in this method.
+ * @return array Updated list of tests contained within this collection.
*/
public function add($test = null, array $options = array()) {
- $callback = function($test) {
- if (empty($test)) {
- return array();
- }
- if (is_object($test) && $test instanceof \lithium\test\Unit) {
- return array(get_class($test));
- }
- if (is_string($test)) {
- if ($test[0] != '\\') {
- $test = "lithium\\tests\cases\\{$test}";
- }
- if (preg_match("/Test/", $test)) {
- return array($test);
- }
- $parts = array_filter(explode("\\", $test));
- $library = array_shift($parts);
- $test = Libraries::find($library, array(
- 'recursive' => true,
- 'path' => '/' . join('/', $parts),
- 'filter' => '/cases|intergration|functional/'
- ));
- return (array) $test;
+ $resolve = function($self, $test) {
+ switch (true) {
+ case !$test:
+ return array();
+ case is_object($test) && $test instanceof Unit:
+ return array(get_class($test));
+ case is_string($test) && !file_exists(Libraries::path($test)):
+ return $self->invokeMethod('_resolve', array($test));
+ default:
+ return (array) $test;
}
- return (array) $test;
};
-
if (is_array($test)) {
foreach ($test as $t) {
- $this->_items = array_filter(array_merge($this->_items, $callback($t)));
+ $this->_data = array_filter(array_merge($this->_data, $resolve($this, $t)));
}
- return $this->_items;
+ return $this->_data;
}
- return $this->_items = array_merge($this->_items, $callback($test));
+ return $this->_data = array_merge($this->_data, $resolve($this, $test));
}
/**
@@ -101,9 +102,44 @@ class Group extends \lithium\util\Collection {
*/
public function tests($params = array(), array $options = array()) {
$tests = new Collection();
- array_map(function($test) use ($tests) { $tests[] = new $test; }, $this->_items);
+
+ foreach ($this->_data as $test) {
+ if (!class_exists($test)) {
+ throw new Exception("Test case `{$test}` not found.");
+ }
+ $tests[] = new $test;
+ }
return $tests;
}
+
+ /**
+ * Resolves a unit test class (or classes) from a class or namespace path string.
+ *
+ * @param string $test The path string in which to find the test case(s). This may be a
+ * library, a namespace, or a fully-namespaced class reference.
+ * @return array Returns an array containing one or more fully-namespaced class references to
+ * unit tests.
+ */
+ protected function _resolve($test) {
+ if (strpos($test, '\\') === false && Libraries::get($test)) {
+ return (array) Libraries::find($test, array(
+ 'recursive' => true, 'filter' => '/cases|integration|functional/',
+ ));
+ }
+ if (preg_match("/Test/", $test)) {
+ return array($test);
+ }
+ if (!$test = trim($test, '\\')) {
+ return array();
+ }
+ list($library, $path) = explode('\\', $test, 2) + array($test, null);
+
+ return (array) Libraries::find($library, array(
+ 'recursive' => true,
+ 'path' => '/' . str_replace('\\', '/', $path),
+ 'filter' => '/cases|integration|functional/',
+ ));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/test/Integration.php b/libraries/lithium/test/Integration.php
new file mode 100644
index 0000000..707f012
--- /dev/null
+++ b/libraries/lithium/test/Integration.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test;
+
+/**
+ * This is the base class for integration tests.
+ *
+ * Integration tests are for determining that different parts of the framework will work
+ * together (integrate) as expected. An example of a common integration test would be for
+ * ensuring that an adapter interacts correctly with the class it is designed to interface
+ * with. Example: the `Session` class and the `Php` adapter. Unit tests will ensure that
+ * both the `Session` and `Php` classes behave correctly under isolation, while an integration
+ * test ensures that the two classes interact and interface correctly.
+ *
+ */
+class Integration extends \lithium\test\Unit {
+
+ /**
+ * Auto init for applying Integration filter to this test class.
+ *
+ * @return void
+ */
+ protected function _init() {
+ parent::_init();
+
+ $this->applyFilter('run', function($self, $params, $chain) {
+ $before = $self->results();
+
+ $chain->next($self, $params, $chain);
+
+ $after = $self->results();
+
+ while (count($after) > count($before)) {
+ $result = array_pop($after);
+ if ($result['result'] == 'fail') {
+ return false;
+ }
+ }
+ });
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/Report.php b/libraries/lithium/test/Report.php
index 57a96dc..e2ba7a0 100644
--- a/libraries/lithium/test/Report.php
+++ b/libraries/lithium/test/Report.php
@@ -2,20 +2,63 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test;
-use \Exception;
-use \lithium\core\Libraries;
-use \lithium\util\Inflector;
+use lithium\core\Libraries;
+use lithium\util\Inflector;
+use lithium\core\ClassNotFoundException;
/**
- * Report object for running group tests holding results
+ * This `Report` object aggregates tests in a group and allows you to run said tests to
+ * obtain the results and stats (passes, fails, exceptions, skips) of the test run.
*
- * @package default
+ * While Lithium already comes with a text-based as well as web-based test interface, you
+ * may use or extend the `Report` class to create your own test reporter functionality. In
+ * addition, you can also create your own custom templates for displaying results in a different
+ * format, such as json.
+ *
+ * Example usage, for built-in HTML format/reporter:
+ *
+ * {{{
+ * $report = new Report(array(
+ * 'title' => 'Test Report Title',
+ * 'group' => new Group(array('data' => array('\lithium\tests\cases\net\http\MediaTest'))),
+ * 'format' => 'html',
+ * 'reporter' => 'html'
+ * ));
+ *
+ * $report->run();
+ *
+ * //Get the test stats:
+ * $report->stats();
+ *
+ * //Get test results:
+ * $report->results
+ * }}}
+ *
+ * You may also choose to filter the results of the test runs to obtain additional information.
+ * For example, say you wish to calculate the cyclomatic complexity of the classes you are testing:
+ *
+ * {{{
+ * $report = new Report(array(
+ * 'title' => 'Test Report Title',
+ * 'group' => new Group(array('data' => array('\lithium\tests\cases\net\http\MediaTest'))),
+ * 'filters' => array('Complexity')
+ * ));
+ *
+ * $report->run();
+ *
+ * //Get test results, including filter results:
+ * $report->results
+ * }}}
+ *
+ * @see lithium\test\Group
+ * @see lithium\test\filter
+ * @see lithium\test\templates
*/
class Report extends \lithium\core\Object {
@@ -28,21 +71,6 @@ class Report extends \lithium\core\Object {
public $group = null;
/**
- * Contains an instance of a test reporter, which contains the format to be displayed.
- *
- * @var object
- */
- public $reporter = null;
-
- /**
- * An array of fully-namespaced class names representing the filters to be applied to this test
- * group.
- *
- * @var array
- */
- public $filters = array();
-
- /**
* Title of the group being run
*
* @var string
@@ -64,6 +92,14 @@ class Report extends \lithium\core\Object {
public $timer = array('start' => null, 'end' => null);
/**
+ * An array key on fully-namespaced class names of the filter with options to be
+ * applied for the filter as the value
+ *
+ * @var array
+ */
+ protected $_filters = array();
+
+ /**
* Construct Report Object
*
* @param array $config Options array for the test run. Valid options are:
@@ -75,9 +111,10 @@ class Report extends \lithium\core\Object {
'title' => null,
'group' => null,
'filters' => array(),
- 'reporter' => 'text'
+ 'format' => 'txt',
+ 'reporter' => 'console',
);
- parent::__construct((array) $config + $defaults);
+ parent::__construct($config + $defaults);
}
/**
@@ -86,14 +123,7 @@ class Report extends \lithium\core\Object {
* @return void
*/
protected function _init() {
- $class = Inflector::camelize($this->_config['reporter']);
-
- if (!$reporter = Libraries::locate('test.reporter', $class)) {
- throw new Exception("{$class} is not a valid reporter");
- }
- $this->reporter = new $reporter();
$this->group = $this->_config['group'];
- $this->filters = $this->_config['filters'];
$this->title = $this->_config['title'] ?: $this->_config['title'];
}
@@ -104,31 +134,15 @@ class Report extends \lithium\core\Object {
*/
public function run() {
$tests = $this->group->tests();
- $filters = array();
- foreach ($this->filters as $filter => $options) {
- if (!$class = Libraries::locate('test.filter', $filter)) {
- throw new Exception("{$class} is not a valid test filter.");
- }
- $options = isset($options['apply']) ? $options['apply'] : array();
- $tests = $class::apply($this, $options) ?: $tests;
- $filters[] = compact('class', 'options');
+ foreach ($this->filters() as $filter => $options) {
+ $this->results['filters'][$filter] = array();
+ $tests = $filter::apply($this, $tests, $options['apply']) ?: $tests;
}
$this->results['group'] = $tests->run();
- foreach ($filters as $filter) {
- if (isset($filter['options']['analyze'])) {
- $filter['options'] = $filter['options']['analyze'];
- } else {
- $filter['options'] = array();
- }
- if (!isset($this->results['filters'][$filter['class']])) {
- $this->results['filters'][$filter['class']] = array();
- }
- $this->results['filters'][$filter['class']] = $filter['class']::analyze(
- $this,
- $filter['options']
- );
+ foreach ($this->filters() as $filter => $options) {
+ $this->results['filters'][$filter] = $filter::analyze($this, $options['analyze']);
}
}
@@ -141,9 +155,6 @@ class Report extends \lithium\core\Object {
* @return void
*/
public function collect($class, $results) {
- if (!isset($this->results['filters'][$class])) {
- $this->results['filters'][$class] = array();
- }
$this->results['filters'][$class][] = $results;
}
@@ -154,15 +165,16 @@ class Report extends \lithium\core\Object {
*/
public function stats() {
$results = (array) $this->results['group'];
- return $this->reporter->stats(array_reduce($results, function($stats, $result) {
- $stats = (array) $stats + array(
- 'asserts' => 0,
- 'passes' => array(),
- 'fails' => array(),
- 'exceptions' => array(),
- 'errors' => array(),
- 'skips' => array()
- );
+ $defaults = array(
+ 'asserts' => 0,
+ 'passes' => array(),
+ 'fails' => array(),
+ 'exceptions' => array(),
+ 'errors' => array(),
+ 'skips' => array()
+ );
+ $stats = array_reduce($results, function($stats, $result) use ($defaults) {
+ $stats = (array) $stats + $defaults;
$result = empty($result[0]) ? array($result) : $result;
foreach ($result as $response) {
if (empty($response['result'])) {
@@ -171,6 +183,9 @@ class Report extends \lithium\core\Object {
$result = $response['result'];
if (in_array($result, array('fail', 'exception'))) {
+ $response = array_merge(
+ array('class' => 'unknown', 'method' => 'unknown'), $response
+ );
$stats['errors'][] = $response;
}
unset($response['file'], $response['result']);
@@ -183,16 +198,55 @@ class Report extends \lithium\core\Object {
}
}
return $stats;
- }));
+ });
+ $stats = (array) $stats + $defaults;
+ $count = array_map(
+ function($value) { return is_array($value) ? count($value) : $value; }, $stats
+ );
+ $success = $count['passes'] == $count['asserts'] && $count['errors'] === 0;
+ return compact("stats", "count", "success");
}
/**
- * undocumented function
+ * Renders the test output (e.g. layouts and filter templates)
*
- * @return void
+ * @param string $template name of the template (eg: layout)
+ * @param string $data array from `_data()` method
+ * @param array $options Array of options (e.g. rendering type)
+ * @return string
*/
- public function filters() {
- return $this->reporter->filters((array) $this->results['filters']);
+ public function render($template, $data = array()) {
+ $config = $this->_config;
+ if ($template == "stats") {
+ $data = $this->stats();
+ }
+ $template = Libraries::locate("test.templates.{$config['reporter']}", $template, array(
+ 'filter' => false, 'type' => 'file', 'suffix' => ".{$config['format']}.php",
+ ));
+ $params = compact('template', 'data', 'config');
+
+ return $this->_filter(__METHOD__, $params, function($self, $params, $chain) {
+ extract($params['data']);
+ ob_start();
+ include $params['template'];
+ return ob_get_clean();
+ });
+ }
+
+ public function filters(array $filters = array()) {
+ if (!empty($this->_filters) && empty($filters)) {
+ return $this->_filters;
+ }
+ $filters += (array) $this->_config['filters'];
+ $results = array();
+ foreach ($filters as $filter => $options) {
+ if (!$class = Libraries::locate('test.filter', $filter)) {
+ throw new ClassNotFoundException("`{$class}` is not a valid test filter.");
+ }
+ $options['name'] = strtolower(join('', array_slice(explode("\\", $class), -1)));
+ $results[$class] = $options + array('apply' => array(), 'analyze' => array());
+ }
+ return $this->_filters = $results;
}
}
diff --git a/libraries/lithium/test/Reporter.php b/libraries/lithium/test/Reporter.php
deleted file mode 100644
index c59f346..0000000
--- a/libraries/lithium/test/Reporter.php
+++ /dev/null
@@ -1,141 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\test;
-
-use \Exception;
-use \lithium\core\Libraries;
-use \lithium\util\Inflector;
-
-/**
- * Reporter class to handle test report output.
- */
-class Reporter extends \lithium\core\Object {
-
- public function stats($stats) {
- $defaults = array(
- 'asserts' => null,
- 'passes' => array(),
- 'fails' => array(),
- 'errors' => array(),
- 'exceptions' => array(),
- 'skips' => array()
- );
- $stats = (array) $stats + $defaults;
-
- $count = array_map(
- function($value) { return is_array($value) ? count($value) : $value; },
- $stats
- );
- $success = $count['passes'] === $count['asserts'] && $count['errors'] === 0;
-
- $result[] = $this->_result($count + compact('success'));
-
- foreach ((array) $stats['errors'] as $error) {
- $error = array_merge(
- array('class' => 'unknown', 'method' => 'unknown'), (array) $error
- );
- $method = "_{$error['result']}";
- $result[] = $this->{$method}($error);
- }
- foreach ((array) $stats['skips'] as $skip) {
- $result[] = $this->_skip($skip);
- }
- return trim(join("\n", $result));
- }
-
- /**
- * Return menu as a string to be used as render.
- *
- * @param array $classes
- * @param array $options
- * - format: type of reporter class. eg: html default: text
- * - tree: true to convert classes to tree structure
- */
- public function menu($classes, array $options = array()) {
- $defaults = array('request' => null, 'tree' => false);
- $options += $defaults;
-
- if ($options['tree']) {
- $data = array();
- $assign = function(&$data, $class, $i = 0) use (&$assign) {
- isset($data[$class[$i]]) ?: $data[] = $class[$i];
- $end = (count($class) <= $i + 1);
-
- if (!$end && ($offset = array_search($class[$i], $data)) !== false) {
- $data[$class[$i]] = array();
- unset($data[$offset]);
- }
- ksort($data);
- $end ?: $assign($data[$class[$i]], $class, $i + 1);
- };
-
- foreach ($classes as $class) {
- $assign($data, explode('\\', str_replace('\tests', '', $class)));
- }
- $classes = $data;
- }
- ksort($classes);
-
- $result = null;
-
- if ($options['tree']) {
- $self = $this;
- $menu = function ($data, $parent = null) use (&$menu, &$self, $result, $options) {
- foreach ($data as $key => $row) {
- if (is_array($row) && is_string($key)) {
- $key = strtolower($key);
- $next = $parent . '/' . $key;
- $result .= $self->invokeMethod('_item', array('group', array(
- 'request' => $options['request'], 'namespace' => $next,
- 'name' => $key, 'menu' => $menu($row, $next)
- )));
- } else {
- $next = $parent . '/' . $key;
- $result .= $self->invokeMethod('_item', array('case', array(
- 'request' => $options['request'], 'namespace' => $parent, 'name' => $row,
- )));
- }
- }
- return $self->invokeMethod('_item', array(null, array('menu' => $result)));
- };
-
- foreach ($classes as $library => $tests) {
- $group = "{$library}/tests";
- $result .= $this->_item(null, array('menu' => $this->_item('group', array(
- 'request' => $options['request'], 'namespace' => $group,
- 'name' => $library, 'menu' => $menu($tests, $group)
- ))));
- }
- return $result;
- }
-
- foreach ($classes as $test) {
- $parts = explode('\\', $test);
- $result .= $this->_item('case', array(
- 'request' => $options['request'], 'name' => array_pop($parts),
- 'namespace' => join('/', $parts)
- ));
- }
- return $this->_item(null, array('menu' => $result));
- }
-
- public function filters($filters) {}
-
- protected function _result($data) {}
-
- protected function _fail($data) {}
-
- protected function _exception($data) {}
-
- protected function _skip($data) {}
-
- protected function _item($data) {}
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/test/Unit.php b/libraries/lithium/test/Unit.php
index 12fcedd..44cf8ff 100644
--- a/libraries/lithium/test/Unit.php
+++ b/libraries/lithium/test/Unit.php
@@ -2,19 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test;
-use \Exception;
-use \lithium\util\String;
-use \lithium\util\Validator;
-use \lithium\analysis\Debugger;
-use \lithium\analysis\Inspector;
-use \RecursiveDirectoryIterator;
-use \RecursiveIteratorIterator;
+use Exception;
+use lithium\util\String;
+use lithium\core\Libraries;
+use lithium\util\Validator;
+use lithium\analysis\Debugger;
+use lithium\analysis\Inspector;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
/**
* This is the base class for all test cases. Test are performed using an assertion method. If the
@@ -61,67 +62,19 @@ class Unit extends \lithium\core\Object {
protected $_expected = array();
/**
- * Runs the test methods in this test case, with the given options.
+ * Finds the test case for the corresponding class name.
*
- * @param array $options The options to use when running the test. Available options are:
- * - 'methods': An arbitrary array of method names to execute. If
- * unspecified, all methods starting with 'test' are run.
- * - 'reporter': A closure which gets called after each test result,
- * which may modify the results presented.
- * @return array
+ * @param string $class A fully-namespaced class reference for which to find a test case.
+ * @return string Returns the class name of a test case for `$class`, or `null` if none exists.
*/
- public function run(array $options = array()) {
- $defaults = array('methods' => array(), 'reporter' => null, 'handler' => null);
- $options += $defaults;
- $this->_results = array();
- $self = $this;
-
- $h = function($code, $message, $file, $line = 0, $context = array()) use ($self) {
- $trace = debug_backtrace();
- $trace = array_slice($trace, 1, count($trace));
-
- $self->invokeMethod('_handleException', array(
- compact('code', 'message', 'file', 'line', 'trace', 'context')
- ));
- };
-
- $options['handler'] = $options['handler'] ?: $h;
- $methods = $options['methods'] ?: $this->methods();
- $this->_reporter = $options['reporter'] ?: $this->_reporter;
-
- try {
- $this->skip();
- } catch (Exception $e) {
- $this->_handleException($e);
- return $this->_results;
- }
- set_error_handler($options['handler']);
+ public static function get($class) {
+ $parts = explode('\\', $class);
- foreach ($methods as $method) {
- $this->_runTestMethod($method, $options);
- }
+ $library = array_shift($parts);
+ $name = array_pop($parts);
+ $type = "tests.cases." . implode('.', $parts);
- restore_error_handler();
- return $this->_results;
- }
-
- /**
- * Returns the class name that is the subject under test for this test case.
- *
- * @return string
- */
- public function subject() {
- return preg_replace('/Test$/', '', str_replace('tests\\cases\\', '', get_class($this)));
- }
-
- /**
- * Return test methods to run
- *
- * @return array
- */
- public function methods() {
- static $methods;
- return $methods ?: $methods = array_values(preg_grep('/^test/', get_class_methods($this)));
+ return Libraries::locate($type, $name, compact('library'));
}
/**
@@ -154,8 +107,7 @@ class Unit extends \lithium\core\Object {
*
* @return void
*/
- public function skip() {
- }
+ public function skip() {}
/**
* Skips test(s) if the condition is met.
@@ -168,27 +120,100 @@ class Unit extends \lithium\core\Object {
* @param string $message Message to pass if the condition is met.
* @return mixed
*/
- public function skipIf($condition, $message = 'Skipped test {:class}::{:function}()') {
+ public function skipIf($condition, $message = 'Skipped test `{:class}::{:function}()`.') {
if (!$condition) {
return;
}
$trace = Debugger::trace(array('start' => 2, 'depth' => 3, 'format' => 'array'));
- throw new Exception(String::insert($message, $trace));
+ throw new Exception(String::insert($message, array_pop($trace)));
}
/**
- * undocumented function
+ * Returns the class name that is the subject under test for this test case.
*
- * @param string $expression
- * @param string $message
- * @param string $data
+ * @return string
+ */
+ public function subject() {
+ return preg_replace('/Test$/', '', str_replace('tests\\cases\\', '', get_class($this)));
+ }
+
+ /**
+ * Return test methods to run
+ *
+ * @return array
+ */
+ public function methods() {
+ static $methods;
+ return $methods ?: $methods = array_values(preg_grep('/^test/', get_class_methods($this)));
+ }
+
+ /**
+ * Runs the test methods in this test case, with the given options.
+ *
+ * @param array $options The options to use when running the test. Available options are:
+ * - 'methods': An arbitrary array of method names to execute. If
+ * unspecified, all methods starting with 'test' are run.
+ * - 'reporter': A closure which gets called after each test result,
+ * which may modify the results presented.
+ * @return array
+ */
+ public function run(array $options = array()) {
+ $defaults = array('methods' => array(), 'reporter' => null, 'handler' => null);
+ $options += $defaults;
+ $this->_results = array();
+ $self = $this;
+
+ try {
+ $this->skip();
+ } catch (Exception $e) {
+ $this->_handleException($e);
+ return $this->_results;
+ }
+
+ $h = function($code, $message, $file, $line = 0, $context = array()) use ($self) {
+ $trace = debug_backtrace();
+ $trace = array_slice($trace, 1, count($trace));
+ $self->invokeMethod('_reportException', array(
+ compact('code', 'message', 'file', 'line', 'trace', 'context')
+ ));
+ };
+ $options['handler'] = $options['handler'] ?: $h;
+ set_error_handler($options['handler']);
+
+ $methods = $options['methods'] ?: $this->methods();
+ $this->_reporter = $options['reporter'] ?: $this->_reporter;
+
+ foreach ($methods as $method) {
+ if ($this->_runTestMethod($method, $options) === false) {
+ break;
+ }
+ }
+ restore_error_handler();
+ return $this->_results;
+ }
+
+ /**
+ * General assert method used by others for common output.
+ *
+ * @param boolean $expression
+ * @param string $message The message to output. If the message is not a string, then it will be
+ * converted to '{:message}'. Use '{:message}' in the string and it will use the `$data`
+ * to format the message with `String::insert()`.
+ * @param array $data
* @return void
*/
- public function assert($expression, $message = '{:message}', $data = array()) {
+ public function assert($expression, $message = false, $data = array()) {
if (!is_string($message)) {
$message = '{:message}';
}
- $trace = Debugger::trace(array('start' => 1, 'format' => 'array'));
+ if (strpos($message, "{:message}") !== false) {
+ $params = $data;
+ $params['message'] = $this->_message($params);
+ $message = String::insert($message, $params);
+ }
+ $trace = Debugger::trace(array(
+ 'start' => 1, 'depth' => 4, 'format' => 'array', 'closures' => !$expression
+ ));
$methods = $this->methods();
$i = 1;
@@ -198,21 +223,15 @@ class Unit extends \lithium\core\Object {
}
$i++;
}
+ $class = isset($trace[$i - 1]['object']) ? get_class($trace[$i - 1]['object']) : null;
- if (strpos($message, "{:message}") !== false) {
- $data['message'] = $this->_message($data);
- }
-
- $result = array(
+ $result = compact('class', 'message', 'data') + array(
'file' => $trace[$i - 1]['file'],
'line' => $trace[$i - 1]['line'],
'method' => $trace[$i]['function'],
'assertion' => $trace[$i - 1]['function'],
- 'class' => get_class($trace[$i - 1]['object']),
- 'message' => String::insert($message, $data),
- 'data' => $data
);
- $this->_result(($expression ? 'pass' : 'fail'), $result);
+ $this->_result($expression ? 'pass' : 'fail', $result);
return $expression;
}
@@ -224,11 +243,8 @@ class Unit extends \lithium\core\Object {
* @param mixed $result
* @param string $message
*/
- public function assertEqual($expected, $result, $message = '{:message}') {
- $data = null;
- if ($expected != $result) {
- $data = $this->_compare('equal', $expected, $result);
- }
+ public function assertEqual($expected, $result, $message = false) {
+ $data = ($expected != $result) ? $this->_compare('equal', $expected, $result) : null;
$this->assert($expected == $result, $message, $data);
}
@@ -239,7 +255,7 @@ class Unit extends \lithium\core\Object {
* @param mixed $result
* @param string $message
*/
- public function assertNotEqual($expected, $result, $message = '{:message}') {
+ public function assertNotEqual($expected, $result, $message = false) {
$this->assert($result != $expected, $message, compact('expected', 'result'));
}
@@ -250,11 +266,8 @@ class Unit extends \lithium\core\Object {
* @param mixed $result
* @param string $message
*/
- public function assertIdentical($expected, $result, $message = '{:message}') {
- $data = null;
- if ($expected !== $result) {
- $data = $this->_compare('identical', $expected, $result);
- }
+ public function assertIdentical($expected, $result, $message = false) {
+ $data = ($expected !== $result) ? $this->_compare('identical', $expected, $result) : null;
$this->assert($expected === $result, $message, $data);
}
@@ -345,9 +358,12 @@ class Unit extends \lithium\core\Object {
*
* Checks for an input tag with a name attribute (contains any non-empty value) and an id
* attribute that contains 'my-input':
+ * {{{
* array('input' => array('name', 'id' => 'my-input'))
+ * }}}
*
* Checks for two p elements with some text in them:
+ * {{{
* array(
* array('p' => true),
* 'textA',
@@ -356,13 +372,16 @@ class Unit extends \lithium\core\Object {
* 'textB',
* '/p'
* )
+ * }}}
*
* You can also specify a pattern expression as part of the attribute values, or the tag
* being defined, if you prepend the value with preg: and enclose it with slashes, like so:
+ * {{{
* array(
* array('input' => array('name', 'id' => 'preg:/FieldName\d+/')),
* 'preg:/My\s+field/'
* )
+ * }}}
*
* Important: This function is very forgiving about whitespace and also accepts any
* permutation of attribute order. It will also allow whitespaces between specified tags.
@@ -442,7 +461,9 @@ class Unit extends \lithium\core\Object {
$attr = $val;
$val = '.+?';
$explanations[] = sprintf('Attribute "%s" present', $attr);
- } elseif (!empty($val) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)) {
+ } elseif (
+ !empty($val) && preg_match('/^regex\:\/(.+)\/$/i', $val, $matches)
+ ) {
$quotes = '"?';
$val = $matches[1];
$explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val);
@@ -483,15 +504,102 @@ class Unit extends \lithium\core\Object {
if (!$matches) {
$this->assert(false, sprintf(
- '{:message} - Item #%d / regex #%d failed: %s', $itemNum, $i, $description
+ '- Item #%d / regex #%d failed: %s', $itemNum, $i, $description
));
return false;
}
}
+ return $this->assert(true);
+ }
+
+ /**
+ * Assert Cookie data is properly set in headers.
+ *
+ * The value passed to `exepected` is an array of the cookie data, with at least the key and
+ * value expected, but can support any of the following keys:
+ * - `key`: the expected key
+ * - `value`: the expected value
+ * - `path`: optionally specifiy a path
+ * - `name`: optionally specify the cookie name
+ * - `expires`: optionally assert a specific expire time
+ *
+ * @param array $expected
+ * @param array $headers When empty, value of `headers_list()` is used.
+ */
+ public function assertCookie($expected, $headers = null) {
+ $matched = $this->_cookieMatch($expected, $headers);
+ if (!$matched['match']) {
+ $message = sprintf('%s - Cookie not found in headers.', $matched['pattern']);
+ $this->assert(false, $message, compact('expected', 'result'));
+ return false;
+ }
+ return $this->assert(true, '%s');
+ }
+
+ /**
+ * Assert Cookie data is *not* set in headers.
+ *
+ * The value passed to `exepected` is an array of the cookie data, with at least the key and
+ * value expected, but can support any of the following keys:
+ * - `key`: the expected key
+ * - `value`: the expected value
+ * - `path`: optionally specifiy a path
+ * - `name`: optionally specify the cookie name
+ * - `expires`: optionally assert a specific expire time
+ *
+ * @param array $expected
+ * @param array $headers When empty, value of `headers_list()` is used.
+ */
+ public function assertNoCookie($expected, $headers = null) {
+ $matched = $this->_cookieMatch($expected, $headers);
+ if ($matched['match']) {
+ $message = sprintf('%s - Cookie not found in headers.', $matched['pattern']);
+ $this->assert(false, $message, compact('expected', 'result'));
+ return false;
+ }
return $this->assert(true, '%s');
}
/**
+ * Match an `$expected` cookie with the given headers. If no headers are provided, then
+ * the value of `headers_list()` will be used.
+ *
+ * @param array $expected
+ * @param array $headers When empty, value of `headers_list()` will be used.
+ * @return boolean True if cookie is found, false otherwise.
+ */
+ protected function _cookieMatch($expected, $headers) {
+ $defaults = array('path' => '/', 'name' => '[\w.-]+');
+ $expected += $defaults;
+
+ $headers = ($headers) ?: headers_list();
+ $value = preg_quote(urlencode($expected['value']), '/');
+
+ $key = explode('.', $expected['key']);
+ $key = (count($key) == 1) ? '[' . current($key) . ']' : ('[' . join('][', $key) . ']');
+ $key = preg_quote($key, '/');
+
+ if (isset($expected['expires'])) {
+ $date = gmdate('D, d-M-Y H:i:s \G\M\T', strtotime($expected['expires']));
+ $expires = preg_quote($date, '/');
+ } else {
+ $expires = '(?:.+?)';
+ }
+ $path = preg_quote($expected['path'], '/');
+ $pattern = "/^Set\-Cookie:\s{$expected['name']}$key=$value;";
+ $pattern .= "\sexpires=$expires;\spath=$path/";
+ $match = false;
+
+ foreach ($headers as $header) {
+ if (preg_match($pattern, $header)) {
+ $match = true;
+ continue;
+ }
+ }
+ return compact('match', 'pattern');
+ }
+
+ /**
* Used before a call to `assert*()` if you expect the test assertion to generate an exception
* or PHP error. If no error or exception is thrown, a test failure will be reported. Can
* be called multiple times per assertion, if more than one error is expected.
@@ -532,7 +640,7 @@ class Unit extends \lithium\core\Object {
*
* @param string $method The name of the test method to run.
* @param array $options
- * @return void
+ * @return void | false
*/
protected function _runTestMethod($method, $options) {
try {
@@ -543,7 +651,7 @@ class Unit extends \lithium\core\Object {
}
$params = compact('options', 'method');
- $this->_filter(__CLASS__ . '::run', $params, function($self, $params, $chain) {
+ $passed = $this->_filter(__CLASS__ . '::run', $params, function($self, $params, $chain) {
try {
$method = $params['method'];
$lineFlag = __LINE__ + 1;
@@ -553,6 +661,8 @@ class Unit extends \lithium\core\Object {
}
});
$this->tearDown();
+
+ return $passed;
}
/**
@@ -561,6 +671,8 @@ class Unit extends \lithium\core\Object {
* is found, the expectation is removed from the stack and the error is ignored. If no match
* is found, then the error data is logged to the test results.
*
+ * @see lithium\test\Unit::expectException()
+ * @see lithium\test\Unit::_reportException()
* @param mixed $exception An `Exception` object instance, or an array containing the following
* keys: `'message'`, `'file'`, `'line'`, `'trace'` (in `debug_backtrace()`
* format) and optionally `'code'` (error code number) and `'context'` (an array
@@ -568,10 +680,10 @@ class Unit extends \lithium\core\Object {
* @param integer $lineFlag A flag used for determining the relevant scope of the call stack.
* Set to the line number where test methods are called.
* @return void
- * @see lithium\test\Unit::expectException()
- * @see lithium\test\Unit::_reportException()
*/
protected function _handleException($exception, $lineFlag = null) {
+ $data = $exception;
+
if (is_object($exception)) {
$data = array();
@@ -585,31 +697,30 @@ class Unit extends \lithium\core\Object {
if ($ref['class'] == __CLASS__ && $ref['function'] == 'skipIf') {
return $this->_result('skip', $data);
}
- $exception = $data;
- }
- $message = $exception['message'];
-
- $isExpected = (($exp = end($this->_expected)) && ($exp === true || $exp == $message || (
- Validator::isRegex($exp) && preg_match($exp, $message)
- )));
-
- if ($isExpected) {
- return array_pop($this->_expected);
}
- $this->_reportException($exception, $lineFlag);
+ return $this->_reportException($data, $lineFlag);
}
/**
* Convert an exception object to an exception result array for test reporting.
*
- * @param object $exception The exception object to report on. Statistics are gathered and
+ * @param array $exception The exception data to report on. Statistics are gathered and
* added to the reporting stack contained in `Unit::$_results`.
* @param string $lineFlag
* @return void
* @todo Refactor so that reporters handle trace formatting.
*/
protected function _reportException($exception, $lineFlag = null) {
+ $message = $exception['message'];
+
+ $isExpected = (($exp = end($this->_expected)) && ($exp === true || $exp == $message || (
+ Validator::isRegex($exp) && preg_match($exp, $message)
+ )));
+ if ($isExpected) {
+ return array_pop($this->_expected);
+ }
$initFrame = current($exception['trace']) + array('class' => '-', 'function' => '-');
+
foreach ($exception['trace'] as $frame) {
if (isset($scopedFrame)) {
break;
@@ -621,21 +732,20 @@ class Unit extends \lithium\core\Object {
$scopedFrame = $frame;
}
}
- $trace = $exception['trace'];
- unset($exception['trace']);
-
- $this->_result('exception', $exception + array(
- 'class' => $initFrame['class'],
- 'method' => $initFrame['function'],
- 'trace' => Debugger::trace(array(
- 'trace' => $trace,
+ if (class_exists('lithium\analysis\Debugger')) {
+ $exception['trace'] = Debugger::trace(array(
+ 'trace' => $exception['trace'],
'format' => '{:functionRef}, line {:line}',
'includeScope' => false,
'scope' => array_filter(array(
'functionRef' => __NAMESPACE__ . '\{closure}',
'line' => $lineFlag
)),
- ))
+ ));
+ }
+ $this->_result('exception', $exception + array(
+ 'class' => $initFrame['class'],
+ 'method' => $initFrame['function']
));
}
@@ -651,37 +761,65 @@ class Unit extends \lithium\core\Object {
* @return array Data with the keys `trace'`, `'expected'` and `'result'`.
*/
protected function _compare($type, $expected, $result = null, $trace = null) {
- $types = array(
- 'trace' => $trace, 'expected' => gettype($expected), 'result' => gettype($result)
- );
- if ($types['expected'] !== $types['result']) {
+ $compareTypes = function($expected, $result, $trace) {
+ $types = array('expected' => gettype($expected), 'result' => gettype($result));
+
+ if ($types['expected'] !== $types['result']) {
+ $expected = trim("({$types['expected']}) " . print_r($expected, true));
+ $result = trim("({$types['result']}) " . print_r($result, true));
+ return compact('trace', 'expected', 'result');
+ }
+ };
+ if ($types = $compareTypes($expected, $result, $trace)) {
return $types;
}
-
$data = array();
- $isObject = false;
- if (is_object($expected)) {
- $isObject = true;
- $expected = (array) $expected;
- $result = (array) $result;
- }
-
- if (is_array($expected)) {
+ if (!is_scalar($expected)) {
foreach ($expected as $key => $value) {
- $check = array_key_exists($key, $result) ? $result[$key] : false;
- $newTrace = (($isObject == true) ? "{$trace}->{$key}" : "{$trace}[{$key}]");
+ $newTrace = "{$trace}[{$key}]";
+ $isObject = false;
+ if (is_object($expected)) {
+ $isObject = true;
+ $expected = (array) $expected;
+ $result = (array) $result;
+ }
+ if (!array_key_exists($key, $result)) {
+ $trace = (!$key) ? null : $newTrace;
+ $expected = (!$key) ? $expected : $value;
+ $result = ($key) ? null : $result;
+ return compact('trace', 'expected', 'result');
+ }
+ $check = $result[$key];
+
+ if ($isObject) {
+ $newTrace = ($trace) ? "{$trace}->{$key}" : $key;
+ $expected = (object) $expected;
+ $result = (object) $result;
+ }
if ($type === 'identical') {
if ($value === $check) {
+ if ($types = $compareTypes($value, $check, $trace)) {
+ return $types;
+ }
continue;
}
- if ($check === false) {
+ if ($check === array()) {
$trace = $newTrace;
return compact('trace', 'expected', 'result');
}
+ if (is_string($check)) {
+ $trace = $newTrace;
+ $expected = $value;
+ $result = $check;
+ return compact('trace', 'expected', 'result');
+ }
} else {
if ($value == $check) {
+ if ($types = $compareTypes($value, $check, $trace)) {
+ return $types;
+ }
continue;
}
if (!is_array($value)) {
@@ -695,48 +833,64 @@ class Unit extends \lithium\core\Object {
$data[] = $compare;
}
}
- if (empty($data)) {
- return compact('trace', 'expected', 'result');
+ if (!empty($data)) {
+ return $data;
}
- return $data;
}
-
- if ($type === 'identical') {
- if ($expected === $result) {
- return true;
+ if (!is_scalar($result)) {
+ $data = $this->_compare($type, $result, $expected);
+
+ if (!empty($data)) {
+ return array(
+ 'trace' => $data['trace'],
+ 'expected' => $data['result'],
+ 'result' => $data['expected']
+ );
}
- } else {
- if ($expected == $result) {
- return true;
+ }
+ if ((($type === 'identical') ? $expected === $result : $expected == $result)) {
+ if ($types = $compareTypes($expected, $result, $trace)) {
+ return $types;
}
+ return true;
}
- $data = compact('trace', 'expected', 'result');
- return $data;
+ return compact('trace', 'expected', 'result');
}
/**
* Returns a basic message for the data returned from `_result()`.
*
- * @param array $data The data to use for creating the message.
- * @return string
* @see lithium\test\Unit::assert()
* @see lithium\test\Unit::_result()
+ * @param array $data The data to use for creating the message.
+ * @param string $message The string prepended to the generate message in the current scope.
+ * @return string
*/
- protected function _message($data = array()) {
- $messages = null;
+ protected function _message(&$data = array(), $message = null) {
if (!empty($data[0])) {
- foreach ($data as $message) {
- $messages .= $this->_message($message);
+ foreach ($data as $key => $value) {
+ $message = (!empty($data[$key][0])) ? $message : null;
+ $message .= $this->_message($value, $message);
+ unset($data[$key]);
}
- return $messages;
+ return $message;
}
-
$defaults = array('trace' => null, 'expected' => null, 'result' => null);
- $data = (array) $data + $defaults;
- return sprintf("trace: %s\nexpected: %s\nresult: %s\n",
- $data['trace'],
- var_export($data['expected'], true),
- var_export($data['result'], true)
+ $result = (array) $data + $defaults;
+
+ $message = null;
+ if (!empty($result['trace'])) {
+ $message = sprintf("trace: %s\n", $result['trace']);
+ }
+ if (is_object($result['expected'])) {
+ $result['expected'] = get_object_vars($result['expected']);
+ }
+ if (is_object($result['result'])) {
+ $result['result'] = get_object_vars($result['result']);
+ }
+ return $message . sprintf("expected: %s\nresult: %s\n",
+ var_export($result['expected'], true),
+ var_export($result['result'], true)
);
}
@@ -779,18 +933,31 @@ class Unit extends \lithium\core\Object {
* @return void
*/
protected function _cleanUp($path = null) {
- $path = $path ?: LITHIUM_APP_PATH . '/resources/tmp/tests';
- $path = $path[0] !== '/' ? LITHIUM_APP_PATH . '/resources/tmp/' . $path : $path;
+ $path = $path ?: Libraries::get(true, 'resources') . '/tmp/tests';
+ $path = $path[0] !== '/' ? Libraries::get(true, 'resources') . '/tmp/' . $path : $path;
+
if (!is_dir($path)) {
return;
}
$dirs = new RecursiveDirectoryIterator($path);
$iterator = new RecursiveIteratorIterator($dirs, RecursiveIteratorIterator::CHILD_FIRST);
+
foreach ($iterator as $item) {
- if ($item->getPathname() === "{$path}/empty") continue;
+ if ($item->getPathname() === "{$path}/empty" || $iterator->isDot()) {
+ continue;
+ }
($item->isDir()) ? rmdir($item->getPathname()) : unlink($item->getPathname());
}
}
+
+ /**
+ * Returns the current results
+ *
+ * @return array The Results, currently
+ */
+ public function results() {
+ return $this->_results;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/test/filter/Affected.php b/libraries/lithium/test/filter/Affected.php
index 4bbcf89..b9ce1fa 100644
--- a/libraries/lithium/test/filter/Affected.php
+++ b/libraries/lithium/test/filter/Affected.php
@@ -2,12 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test\filter;
+use lithium\test\Unit;
use lithium\core\Libraries;
use lithium\analysis\Inspector;
@@ -19,7 +20,6 @@ use lithium\analysis\Inspector;
* 1. Looking at the subject of a test case.
* 2. Searching the class tree for any classes that directly depend on that subject.
* 3. Assigning test cases to those classes.
- *
*/
class Affected extends \lithium\test\Filter {
@@ -30,11 +30,11 @@ class Affected extends \lithium\test\Filter {
* instances. Adds affected tests to the test collection.
*
* @param object $report Instance of Report which is calling apply.
+ * @param array $tests The test to apply this filter on
* @param array $options Not used.
* @return object|void Returns the instance of `$tests`.
*/
- public static function apply($report, array $options = array()) {
- $tests = $report->group->tests();
+ public static function apply($report, $tests, array $options = array()) {
$affected = array();
$testsClasses = $tests->map('get_class', array('collect' => false));
@@ -44,7 +44,7 @@ class Affected extends \lithium\test\Filter {
$affected = array_unique($affected);
foreach ($affected as $class) {
- $test = self::_testCaseForClass($class);
+ $test = Unit::get($class);
if ($test && !in_array($test, $testsClasses)) {
$tests[] = new $test();
@@ -59,52 +59,31 @@ class Affected extends \lithium\test\Filter {
*
* @param object $report The report instance running this filter and aggregating results
* @param array $options
- * @return array|void The results of the analysis.
+ * @return array The results of the analysis.
*/
public static function analyze($report, array $options = array()) {
- return $report->results['filters'][__CLASS__];
- }
-
- public static function output($format, $analysis) {
- $analysis = isset($analysis[0]) ? $analysis[0] : array();
- $output = array();
-
- if ($format == 'html') {
- $output[] = "<h3>Additional Affected Tests</h3>";
- $output[] = "<ul class=\"metrics\">";
-
- foreach ($analysis as $class => $test) {
- if ($test) {
- $output[] = "<li>{$test}</li>";
- }
- }
- $output[] = "</ul>";
- } elseif ($format == 'text') {
- $output[] = "Additional Affected Tests";
- $output[] = "-------------------------";
-
- foreach ($analysis as $class => $test) {
- if ($test) {
- $output[] = " - {$test}";
- }
+ $analyze = array();
+ foreach ($report->results['filters'][__CLASS__] as $result) {
+ foreach ($result as $class => $test) {
+ $analyze[$class] = $test;
}
}
- return implode("\n", $output);
+
+ return $analyze;
}
/**
* Returns all classes directly depending on a given class.
*
* @param string $dependency The class name to use as a dependency.
+ * @param string $exclude Regex path exclusion filter.
* @return array Classes having a direct dependency on `$dependency`. May cotain duplicates.
*/
- protected static function _affected($dependency) {
- $classes = Libraries::find(true, array(
- 'recursive' => true,
- 'exclude' => '/(tests|webroot|resources|libraries|plugins)/'
- ));
- $affected = array();
+ protected static function _affected($dependency, $exclude = null) {
+ $exclude = $exclude ?: '/(tests|webroot|resources|libraries|plugins)/';
+ $classes = Libraries::find(true, compact('exclude') + array('recursive' => true));
$dependency = ltrim($dependency, '\\');
+ $affected = array();
foreach ($classes as $class) {
if (isset(static::$_cachedDepends[$class])) {
@@ -121,22 +100,6 @@ class Affected extends \lithium\test\Filter {
}
return $affected;
}
-
- /**
- * Returns corresponding test case for a class, ensuring it actually exists.
- *
- * @param string $class
- * @return string|void
- */
- protected static function _testCaseForClass($class) {
- $parts = explode('\\', $class);
-
- $library = array_shift($parts);
- $name = array_pop($parts);
- $type = "tests.cases." . implode('.', $parts);
-
- return Libraries::locate($type, $name, compact('library'));
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/test/filter/Complexity.php b/libraries/lithium/test/filter/Complexity.php
index 3e75630..8fb1360 100644
--- a/libraries/lithium/test/filter/Complexity.php
+++ b/libraries/lithium/test/filter/Complexity.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test\filter;
-use \lithium\analysis\Parser;
-use \lithium\analysis\Inspector;
+use lithium\analysis\Parser;
+use lithium\analysis\Inspector;
/**
* Calculates the cyclomatic complexity of class methods, and shows worst-offenders and statistics.
@@ -29,19 +29,18 @@ class Complexity extends \lithium\test\Filter {
* instances. Introspects the test subject classes to extract cyclomatic complexity data.
*
* @param object $report Instance of Report which is calling apply.
+ * @param array $tests The test to apply this filter on
* @param array $options Not used.
* @return object|void Returns the instance of `$tests`.
*/
- public static function apply($report, array $options = array()) {
- $tests = $report->group->tests();
+ public static function apply($report, $tests, array $options = array()) {
$results = array();
foreach ($tests->invoke('subject') as $class) {
$results[$class] = array();
- if (!$methods = Inspector::methods($class, 'ranges')) {
+ if (!$methods = Inspector::methods($class, 'ranges', array('public' => false))) {
continue;
}
-
foreach ($methods as $method => $lines) {
$lines = Inspector::lines($class, $lines);
$branches = Parser::tokenize(join("\n", (array) $lines), array(
@@ -82,45 +81,6 @@ class Complexity extends \lithium\test\Filter {
}
/**
- * Returns data to be output by a reporter.
- *
- * @param string $format I.e. `'html'` or `'text'`.
- * @param array $analysis The results of the analysis.
- * @return string|void
- */
- public static function output($format, $analysis) {
- $output = null;
-
- if ($format == 'html') {
- $output .= '<h3>Cyclomatic Complexity</h3>';
- $output .= '<table class="metrics"><tbody>';
-
- foreach (array_slice($analysis['max'], 0, 10) as $method => $count) {
- if ($count <= 7) {
- continue;
- }
- $output .= '<tr>';
- $output .= '<td class="metric-name">Worst Offender</th>';
- $output .= '<td class="metric">' . $method . ' - ' . $count . '</td>';
- $output .= '</tr>';
- }
-
- $output .= '<tr>';
- $output .= '<th colspan="2">Class Averages</th>';
- $output .= '</tr>';
-
- foreach (array_slice($analysis['class'], 0, 10) as $class => $count) {
- $output .= '<tr>';
- $output .= '<td class="metric-name">' . $class . '</th>';
- $output .= '<td class="metric">' . round($count, 2) . '</td>';
- $output .= '</tr>';
- }
- $output .= '</tbody></table>';
- }
- return $output;
- }
-
- /**
* Collects raw data aggregated in Report and prepares it for analysis
*
* @param array $filterResults The results of the filter on the test run.
@@ -130,11 +90,13 @@ class Complexity extends \lithium\test\Filter {
$packagedResults = array();
foreach ($filterResults as $result) {
- $class = key($result);
- if (!isset($packagedResults[$class])) {
- $packagedResults[$class] = array();
+ foreach($result as $class => $method) {
+ if (!isset($packagedResults[$class])) {
+ $packagedResults[$class] = array();
+ }
+ $classResult = (array) $result[$class];
+ $packagedResults[$class] = array_merge($classResult, $packagedResults[$class]);
}
- $packagedResults[$class] = array_merge($result[$class], $packagedResults[$class]);
}
return $packagedResults;
diff --git a/libraries/lithium/test/filter/Coverage.php b/libraries/lithium/test/filter/Coverage.php
index 7e7f1d6..29fdc49 100644
--- a/libraries/lithium/test/filter/Coverage.php
+++ b/libraries/lithium/test/filter/Coverage.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test\filter;
-use \lithium\core\Libraries;
-use \lithium\util\String;
-use \lithium\util\Collection;
-use \lithium\analysis\Inspector;
+use lithium\core\Libraries;
+use lithium\util\String;
+use lithium\util\Collection;
+use lithium\analysis\Inspector;
/**
* Runs code coverage analysis for the executed tests.
@@ -24,6 +24,7 @@ class Coverage extends \lithium\test\Filter {
*
* @see lithium\test\filter\Coverage::collect()
* @param object $report Instance of Report which is calling apply.
+ * @param array $tests The test to apply this filter on
* @param array $options Options for how code coverage should be applied. These options are
* also passed to `Coverage::collect()` to determine how to aggregate results. See
* the documentation for `collect()` for further options. Options affecting this
@@ -32,8 +33,7 @@ class Coverage extends \lithium\test\Filter {
* @return object|void Returns the instance of `$tests` with code coverage analysis
* triggers applied.
*/
- public static function apply($report, array $options = array()) {
- $tests = $report->group->tests();
+ public static function apply($report, $tests, array $options = array()) {
$defaults = array('method' => 'run');
$options += $defaults;
$m = $options['method'];
@@ -58,20 +58,21 @@ class Coverage extends \lithium\test\Filter {
* instances each line was called.
*/
public static function analyze($report, array $classes = array()) {
- $filterResults = static::collect($report->results['filters'][__CLASS__]);
- $classes = $classes ?: array_filter(get_declared_classes(), function($class) {
- return (!is_subclass_of($class, 'lithium\test\Unit'));
+ $data = static::collect($report->results['filters'][__CLASS__]);
+ $classes = $classes ?: array_filter(get_declared_classes(), function($class) use ($data) {
+ $unit = 'lithium\test\Unit';
+ return (!(is_subclass_of($class, $unit)) || array_key_exists($class, $data));
});
- $classes = array_values(array_intersect((array) $classes, array_keys($filterResults)));
+ $classes = array_values(array_intersect((array) $classes, array_keys($data)));
$densities = $result = array();
foreach ($classes as $class) {
$classMap = array($class => Libraries::path($class));
- $densities += static::_density($filterResults[$class], $classMap);
+ $densities += static::_density($data[$class], $classMap);
}
$executableLines = array();
- if (!empty($classes)) {
+ if ($classes) {
$executableLines = array_combine($classes, array_map(
function($cls) { return Inspector::executable($cls, array('public' => false)); },
$classes
@@ -85,27 +86,25 @@ class Coverage extends \lithium\test\Filter {
$percentage = round(count($covered) / (count($executable) ?: 1), 4) * 100;
$result[$class] = compact('class', 'executable', 'covered', 'uncovered', 'percentage');
}
+
+ $result = static::collectLines($result);
return $result;
}
/**
- * Returns data to be output by a reporter.
+ * Takes the raw line numbers and returns results with the code from
+ * uncovered lines included.
*
- * @param string $format I.e. `'html'` or `'text'`.
- * @param array $analysis The results of the analysis.
- * @return string|void
+ * @param array $result The raw line number results
+ * @return array
*/
- public static function output($format, $analysis) {
- if (empty($analysis)) {
- return null;
- }
+ protected static function collectLines($result) {
$output = null;
$aggregate = array('covered' => 0, 'executable' => 0);
- foreach ($analysis as $class => $coverage) {
+ foreach ($result as $class => $coverage) {
$out = array();
$file = Libraries::path($class);
- $output .= static::stats($format, $class, $coverage);
$aggregate['covered'] += count($coverage['covered']);
$aggregate['executable'] += count($coverage['executable']);
@@ -148,26 +147,18 @@ class Coverage extends \lithium\test\Filter {
);
}
}
- $data = array();
-
- foreach ($out as $line => $row) {
- $row['line'] = $line;
- $data[] = static::_format($format, 'row', $row);
- }
- if (!empty($data)) {
- $output .= static::_format($format, 'file', compact('file', 'data'));
- }
+ $result[$class]['output'][$file] = $out;
}
- return $output;
+ return $result;
}
/**
* Collects code coverage analysis results from `xdebug_get_code_coverage()`.
*
+ * @see lithium\test\Coverage::analyze()
* @param array $filterResults An array of results arrays from `xdebug_get_code_coverage()`.
* @param array $options Set of options defining how results should be collected.
* @return array The packaged filter results.
- * @see lithium\test\Coverage::analyze()
* @todo Implement $options['merging']
*/
public static function collect($filterResults, array $options = array()) {
@@ -197,23 +188,6 @@ class Coverage extends \lithium\test\Filter {
}
/**
- * Returns header for stats.
- *
- * @param string $format I.e. `'html'` or `'text'`.
- * @param string $class
- * @param array $analysis The results of the analysis.
- * @return string|void
- */
- public static function stats($format, $class, $analysis) {
- $covered = count($analysis['covered']) . ' of ' . count($analysis['executable']);
-
- if ($format == 'html') {
- $title = "{$class}: {$covered} lines covered (<em>{$analysis['percentage']}%</em>)";
- return '<h4 class="coverage">' . $title . '</h4>';
- }
- }
-
- /**
* Reduces the results of multiple XDebug code coverage runs into a single 2D array of the
* aggregate line coverage density per file.
*
@@ -230,9 +204,7 @@ class Coverage extends \lithium\test\Filter {
foreach ($runs as $run) {
foreach ($run as $file => $coverage) {
- $file = str_replace('\\', '/', $file);
-
- if (!empty($classMap)) {
+ if ($classMap) {
if (!$class = array_search($file, $classMap)) {
continue;
}
@@ -253,40 +225,6 @@ class Coverage extends \lithium\test\Filter {
}
return $results;
}
-
- /**
- * Returns row or file in specified format
- *
- * @param string $format [required] html,text
- * @param string $type [required] row, file
- * @param array $data [required] from output
- * @return string
- */
- protected static function _format($format, $type, $data) {
- if ($format === 'html') {
- if ($type == 'file') {
- return sprintf(
- '<div class="code-coverage-results"><h4 class="name">%s</h4>%s</div>',
- $data['file'], join("\n", $data['data'])
- );
- }
- return sprintf(
- '<div class="code-line %s">
- <span class="line-num">%d</span>
- <span class="content">%s</span>
- </div>', $data['class'], $data['line'], htmlspecialchars(
- str_replace("\t", " ", $data['data'])
- )
- );
- } elseif ($format === 'text') {
- if ($type == 'file') {
- return sprintf("%s\n\n%s", $data['file'], join("\n", $data['data']));
- }
- return sprintf("%s: line %d\n%s\n\n",
- $data['class'], $data['line'], str_replace("\t", " ", $data['data'])
- );
- }
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/test/filter/Profiler.php b/libraries/lithium/test/filter/Profiler.php
index b70796d..aba9247 100644
--- a/libraries/lithium/test/filter/Profiler.php
+++ b/libraries/lithium/test/filter/Profiler.php
@@ -2,12 +2,17 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\test\filter;
+/**
+ * The `Profiler` filter tracks timing and memory usage information for each test method, and
+ * presents aggregate reports across single test runs. Used for performance-tuning classes and
+ * methods.
+ */
class Profiler extends \lithium\test\Filter {
/**
@@ -69,14 +74,14 @@ class Profiler extends \lithium\test\Filter {
* instances. Allows for preparing tests before they are run.
*
* @param object $report Instance of Report which is calling apply.
+ * @param array $tests The test to apply this filter on
* @param array $options Options for how this filter should be applied. Available options are:
* - `'method'`
* - `'run'`
* - `'checks'`
* @return object|void Returns the instance of `$tests`.
*/
- public static function apply($report, array $options = array()) {
- $tests = $report->group->tests();
+ public static function apply($report, $tests, array $options = array()) {
$defaults = array('method' => 'run', 'checks' => static::$_metrics);
$options += $defaults;
$m = $options['method'];
@@ -155,61 +160,35 @@ class Profiler extends \lithium\test\Filter {
}
}
}
- return $metrics;
- }
- /**
- * Returns data to be output by a reporter.
- *
- * @param string $format I.e. `'html'` or `'text'`.
- * @param array $analysis The results of the analysis.
- * @return string|void
- */
- public static function output($format, $analysis) {
$totals = array();
+ foreach ($metrics as $class => $data) {
+ foreach ($data as $title => $value) {
+ if(isset(static::$_metrics[$title])) {
+ if (isset($totals[$title]['value'])) {
+ $totals[$title]['value'] += $value;
+ } else {
+ $totals[$title]['value'] = $value;
+ }
- foreach ($analysis as $class => $metrics) {
- foreach ($metrics as $title => $value) {
- $totals[$title] = isset($totals[$title]) ? $totals[$title] : 0;
- $totals[$title] += $value;
- }
- }
- $results = array();
- $output = null;
-
- if ($format == 'html') {
- $output .= '<h3>Benchmarks</h3>';
- $output .= '<table class="metrics"><tbody>';
-
- foreach ($totals as $title => $value) {
- if (!isset(static::$_metrics[$title])) {
- continue;
- }
- $formatter = static::$_formatters[static::$_metrics[$title]['format']];
- $output .= '<tr>';
- $output .= '<td class="metric-name">' . $title . '</th>';
- $output .= '<td class="metric">' . $formatter($value) . '</td>';
- $output .= '</tr>';
- }
- $output .= '</tbody></table>';
- } elseif ($format == 'text') {
- foreach ($totals as $title => $value) {
- if (!isset(static::$_metrics[$title])) {
- continue;
+ if (!isset($totals[$title]['format'])) {
+ $format = static::$_metrics[$title]['format'];
+ $totals[$title]['formatter'] = static::$_formatters[$format];
+ }
}
- $formatter = static::$_formatters[static::$_metrics[$title]['format']];
- $output .= $title . ': ' . $formatter($value) . "\n";
}
}
- return $output;
+
+ $metrics['totals'] = $totals;
+ return $metrics;
}
/**
* Add, remove, or modify a profiler check.
*
+ * @see lithium\test\Profiler::$_metrics
* @param mixed $name
* @param string $value
- * @see lithium\test\Profiler::$_metrics
* @return mixed
*/
public function check($name, $value = null) {
diff --git a/libraries/lithium/test/reporter/Console.php b/libraries/lithium/test/reporter/Console.php
deleted file mode 100644
index 5f16d60..0000000
--- a/libraries/lithium/test/reporter/Console.php
+++ /dev/null
@@ -1,120 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\test\reporter;
-
-use lithium\util\String;
-
-class Console extends \lithium\test\Reporter {
-
- /**
- * undocumented function
- *
- * @param array $stats
- * @return string
- */
- protected function _result($stats) {
- $success = $stats['fails'] == 0 && $stats['exceptions'] == 0;
- $result = array(
- ($success ? '{:success}' : '') . "{$stats['passes']} / {$stats['asserts']} passes",
- "{$stats['fails']} " . ((intval($stats['fails']) == 1) ? 'fail' : 'fails') .
- " and {$stats['exceptions']} " .
- ((intval($stats['exceptions']) == 1) ? 'exceptions' : 'exceptions') .
- ($success ? '{:end}' : ''),
- );
- return join("\n", $result);
- }
-
- /**
- * undocumented function
- *
- * @param array $error
- * @return string
- */
- protected function _fail($error) {
- $fail = array(
- "{:error}Assertion '{$error['assertion']}' failed in",
- "{$error['class']}::{$error['method']}()",
- "on line {$error['line']}:{:end}",
- "\n{$error['message']}",
- );
- return join(" ", $fail);
- }
-
- /**
- * undocumented function
- *
- * @param array $error
- * @return string
- */
- protected function _exception($error) {
- $exception = array(
- "{:error}Exception thrown in
- {$error['class']}::{$error['method']}() on line {$error['line']}:{:end}",
- "{$error['message']}",
- );
- if (isset($error['trace']) && !empty($error['trace'])) {
- $exception[] = "Trace: {$error['trace']}";
- }
- return join("\n", $exception);
- }
-
- /**
- * undocumented function
- *
- * @param string $filters
- * @return void
- */
- public function filters($filters) {
- $result = array();
- foreach ((array) $filters as $class => $data) {
- $result[] = $class::output('text', $data);
- }
- $output = array();
- foreach ($result as $level) {
- if (is_array($level)) {
- foreach ($level as $title => $value) {
- if (is_array($value)) {
- $output[] = "{$value['title']}: {$value['value']}";
- } else {
- $output[] = "{$title}: {$value}";
- }
- }
- }
-
- }
- return join("\n", $output);
- }
-
- /**
- * undocumented function
- *
- * @param string $type
- * @param string $params
- * @return void
- */
- public function _item($type, $params = array()) {
- $defaults = array(
- 'namespace' => null, 'name' => null, 'menu' => null
- );
- $params += $defaults;
- $params['namespace'] = str_replace('/', '.', $params['namespace']);
-
- if ($type == 'group') {
- return String::insert(
- "-group {:namespace}\n{:menu}\n", $params
- );
- }
- if ($type == 'case') {
- return String::insert("-case {:namespace}.{:name}\n", $params);
- }
- return String::insert("\n{:menu}\n", $params);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/test/reporter/Html.php b/libraries/lithium/test/reporter/Html.php
deleted file mode 100644
index 4cf2e79..0000000
--- a/libraries/lithium/test/reporter/Html.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\test\reporter;
-
-use lithium\util\String;
-
-class Html extends \lithium\test\Reporter {
-
- protected $_classes = array(
- 'router' => '\lithium\net\http\Router'
- );
-
- protected function _result($stats) {
- $class = ($stats['success'] ? 'success' : 'fail');
- $result = array(
- "<div class=\"test-result test-result-{$class}\">",
- "{$stats['passes']} / {$stats['asserts']} passes, {$stats['fails']} ",
- ((intval($stats['fails']) == 1) ? 'fail' : 'fails') . " and {$stats['exceptions']} ",
- ((intval($stats['exceptions']) == 1) ? 'exception' : 'exceptions'),
- '</div>'
- );
- return join("", $result);
- }
-
- protected function _fail($error) {
- $fail = array(
- '<div class="test-assert test-assert-failed">',
- "Assertion '{$error['assertion']}' failed in ",
- "{$error['class']}::{$error['method']}() on line ",
- "{$error['line']}: ",
- "<span class=\"content\">{$error['message']}</span>",
- '</div>'
- );
- return join("", $fail);
- }
-
- protected function _exception($error) {
- $exception = array(
- '<div class="test-exception">',
- "Exception thrown in {$error['class']}::{$error['method']}() ",
- "on line {$error['line']}: ",
- "<span class=\"content\">{$error['message']}</span>",
- );
- if (isset($error['trace']) && !empty($error['trace'])) {
- $exception[] = "Trace: <span class=\"trace\">{$error['trace']}</span>";
- }
- $exception[] = '</div>';
- return join("", $exception);
- }
-
- protected function _skip($skip) {
- $result = array(
- '<div class="test-skip">',
- "Skip {$skip['trace'][1]['class']}::{$skip['trace'][1]['function']}() ",
- "on line {$skip['trace'][1]['line']}: ",
- "<span class=\"content\">{$skip['message']}</span>",
- "</div>"
- );
- return join("", $result);
- }
-
- public function filters($filters) {
- $result = array();
- foreach ((array) $filters as $class => $data) {
- $result[] = $class::output('html', $data);
- }
- return join("\n", $result);
- }
-
- /**
- * Renders a menu item.
- *
- * @param string $type group, case or null.
- * @param string $options
- * - request: a request object
- * - namespace: namespace for test case
- * - name: test case class name
- * - menu: current menu string for recursive construction
- * @return void
- */
- protected function _item($type, array $options = array()) {
- $defaults = array('request' => null, 'namespace' => null, 'name' => null, 'menu' => null);
- $options += $defaults;
- $router = $this->_classes['router'];
- extract($options);
-
- $url = array('controller' => '\lithium\test\Controller');
-
- if ($type == 'group') {
- $url = $router::match($url + array('args' => $namespace), $request);
- return "<li><a href=\"{$url}\">{$name}</a>{$menu}</li>";
- }
-
- if ($type == 'case') {
- $args = array('args' => "{$namespace}/{$name}");
- $url = $router::match($url + $args, $request);
- return "<li><a href=\"{$url}\">{$name}</a></li>";
- }
- return "<ul>{$menu}</ul>";
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/test/reporter/Text.php b/libraries/lithium/test/reporter/Text.php
deleted file mode 100644
index f71e684..0000000
--- a/libraries/lithium/test/reporter/Text.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\test\reporter;
-
-use lithium\util\String;
-
-class Text extends \lithium\test\Reporter {
-
- protected function _result($stats) {
- $result = array(
- "{$stats['passes']} / {$stats['asserts']} passes",
- "{$stats['fails']} " . ((intval($stats['fails']) == 1) ? 'fail' : 'fails') .
- " and {$stats['exceptions']} " .
- ((intval($stats['exceptions']) == 1) ? 'exceptions' : 'exceptions'),
- );
- return join("\n", $result);
- }
-
- protected function _fail($error) {
- $fail = array(
- "Assertion `{$error['assertion']}` failed in",
- "`{$error['class']}::{$error['method']}()`",
- "on line {$error['line']}:",
- "\n{$error['message']}",
- );
- return join(" ", $fail);
- }
-
- protected function _exception($error) {
- $exception = array(
- "Exception thrown in `{$error['class']}::{$error['method']}()` on line {$error['line']}:",
- "{$error['message']}",
- );
- if (isset($error['trace']) && !empty($error['trace'])) {
- $exception[] = "Trace: {$error['trace']}";
- }
- return join("\n", $exception);
- }
-
- protected function _skip($skip) {
- $result = array(
- "Skip {$skip['trace'][1]['class']}::{$skip['trace'][1]['function']}() ",
- "on line {$skip['trace'][1]['line']}:\n",
- "{$skip['message']}",
- );
- return join("", $result);
- }
-
- public function filters($filters) {
- $output = array();
-
- foreach ((array) $filters as $class => $data) {
- $output[] = $class::output('text', $data);
- }
- return join("\n", $output);
- }
-
- public function _item($type, $params = array()) {
- $defaults = array(
- 'namespace' => null, 'name' => null, 'menu' => null
- );
- $params += $defaults;
- $params['namespace'] = str_replace('/', '.', $params['namespace']);
-
- if ($type == 'group') {
- return String::insert(
- "-group {:namespace}\n{:menu}\n", $params
- );
- }
- if ($type == 'case') {
- return String::insert("-case {:namespace}.{:name}\n", $params);
- }
- return String::insert("\n{:menu}\n", $params);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/test/reporter/template/layout.html.php b/libraries/lithium/test/reporter/template/layout.html.php
deleted file mode 100644
index aa8c922..0000000
--- a/libraries/lithium/test/reporter/template/layout.html.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
- use \lithium\util\Inflector;
- $base = $request->env('base');
-?>
-<!doctype html>
-<html>
- <head>
- <title></title>
- <link rel="stylesheet" href="<?php echo $base;?>/css/debug.css" />
- <link href="<?php echo $base;?>/favicon.ico" title="Icon" type="image/x-icon" rel="icon" />
- <link href="<?php echo $base;?>/favicon.ico" title="Icon" type="image/x-icon" rel="shortcut icon" /></head>
- </head>
- <body class="test-dashboard">
- <h1>Lithium Unit Test Dashboard</h1>
-
- <div style="float: left; padding: 10px 0 20px 20px; width: 20%;">
- <h2><a href="/test/">Tests</a></h2>
- <?php echo $menu ?>
- </div>
-
- <div style="float:left; padding: 10px; width: 75%">
- <h2>Stats for <?php echo $report->title; ?></h2>
-
- <h3>Test results</h3>
-
- <span class="filters">
- <?php echo join(' | ', array_map(
- function($class) use ($request) {
- $url = "?filters[]={$class}";
- $name = join('', array_slice(explode("\\", $class), -1));
- $key = Inflector::underscore($name);
- return "<a class=\"{$key}\" href=\"{$url}\">{$name}</a>";
- },
- $filters
- )); ?>
- </span>
- <?php
- echo $report->stats();
- echo $report->filters();
- ?>
- </div>
- <div style="clear:both"></div>
- </body>
-</html>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/console/affected.txt.php b/libraries/lithium/test/templates/console/affected.txt.php
new file mode 100644
index 0000000..be85998
--- /dev/null
+++ b/libraries/lithium/test/templates/console/affected.txt.php
@@ -0,0 +1,8 @@
+{:heading2}Affected Tests{:end}
+<?php
+ foreach ($data as $class => $test) {
+ if ($test) {
+ echo "{$test}\n";
+ }
+ }
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/console/complexity.txt.php b/libraries/lithium/test/templates/console/complexity.txt.php
new file mode 100644
index 0000000..b980796
--- /dev/null
+++ b/libraries/lithium/test/templates/console/complexity.txt.php
@@ -0,0 +1,16 @@
+{:heading2}Cyclomatic Complexity{:end}
+<?php
+foreach (array_slice($data['max'], 0, 10) as $method => $count) {
+ if ($count <= 7) {
+ continue;
+ }
+ echo "Worst Offender\n\t{$method} - {$count}\n";
+}
+?>
+{:heading3}Class Averages{:end}
+<?php
+foreach (array_slice($data['class'], 0, 10) as $class => $count) {
+ echo "\t{$class} - ";
+ echo round($count, 2) . "\n";
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/console/coverage.txt.php b/libraries/lithium/test/templates/console/coverage.txt.php
new file mode 100644
index 0000000..069c207
--- /dev/null
+++ b/libraries/lithium/test/templates/console/coverage.txt.php
@@ -0,0 +1,11 @@
+{:heading2}Code Coverage{:end}
+<?php
+ foreach ($data as $class => $coverage) {
+ echo ($coverage['percentage'] >= 85 ? "{:success}" : "{:error}");
+ echo "{$class}{:end}: ";
+ echo count($coverage['covered']) . " of " . count($coverage['executable']);
+ echo " lines covered (";
+ echo ($coverage['percentage'] >= 85 ? "{:success}" : "{:error}");
+ echo "{$coverage['percentage']}%{:end})\n";
+ }
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/console/profiler.txt.php b/libraries/lithium/test/templates/console/profiler.txt.php
new file mode 100644
index 0000000..51678d8
--- /dev/null
+++ b/libraries/lithium/test/templates/console/profiler.txt.php
@@ -0,0 +1,6 @@
+{:heading2}Benchmarks{:end}
+<?php
+ foreach ($data['totals'] as $title => $result) {
+ echo "{$title}: {$result['formatter']($result['value'])}\n";
+ }
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/console/stats.txt.php b/libraries/lithium/test/templates/console/stats.txt.php
new file mode 100644
index 0000000..3f2105d
--- /dev/null
+++ b/libraries/lithium/test/templates/console/stats.txt.php
@@ -0,0 +1,32 @@
+<?php
+
+$passes = intval($count['passes']) ?: 0;
+$asserts = intval($count['asserts']) ?: 0;
+$fails = intval($count['fails']) ?: 0;
+$exceptions = intval($count['exceptions']) ?: 0;
+
+echo "\n" . ($success ? '{:success}' : '') . "{$passes} / {$asserts} passes\n";
+echo "{$fails} " . ($fails == 1 ? 'fail' : 'fails');
+echo " and {$exceptions} ";
+echo ($exceptions == 1 ? 'exception' : 'exceptions') . ($success ? '{:end}' : '') . "\n";
+
+foreach ((array) $stats['errors'] as $error) {
+ if ($error['result'] == 'fail') {
+ echo "\n{:error}Assertion '{$error['assertion']}' failed in ";
+ echo "{$error['class']}::{$error['method']}() on line ";
+ echo "{$error['line']}:{:end} \n{$error['message']}";
+ } elseif ($error['result'] == 'exception') {
+ echo "{:error}Exception thrown in {$error['class']}::{$error['method']}()";
+ echo " on line {$error['line']}:{:end} \n{$error['message']}";
+ if (isset($error['trace']) && !empty($error['trace'])) {
+ echo "Trace: {$error['trace']}\n";
+ }
+ }
+}
+foreach ((array) $stats['skips'] as $skip) {
+ $trace = $skip['trace'][1];
+ echo "Skip {$trace['class']}::{$trace['function']}() on line {$trace['line']}:";
+ echo "{$skip['message']}\n";
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/html/affected.html.php b/libraries/lithium/test/templates/html/affected.html.php
new file mode 100644
index 0000000..c0fdc48
--- /dev/null
+++ b/libraries/lithium/test/templates/html/affected.html.php
@@ -0,0 +1,12 @@
+<h3>Affected Tests</h3>
+<ul class="metrics">
+
+<?php foreach ($data as $class => $test): ?>
+ <?php if ($test): ?>
+ <li>
+ <a title="run '<?php echo $test; ?>' tests" href="<?php echo $base ?>/test/<?php echo str_replace('\\', '/', $test); ?>"><?php echo $test ?></a>
+ </li>
+ <?php endif ?>
+<?php endforeach ?>
+
+</ul>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/html/complexity.html.php b/libraries/lithium/test/templates/html/complexity.html.php
new file mode 100644
index 0000000..766ed7e
--- /dev/null
+++ b/libraries/lithium/test/templates/html/complexity.html.php
@@ -0,0 +1,23 @@
+<h3>Cyclomatic Complexity</h3>
+<table class="metrics"><tbody>
+<?php foreach (array_slice($data['max'], 0, 10) as $method => $count): ?>
+ <?php
+ if ($count <= 7) {
+ continue;
+ }
+ ?>
+ <tr>
+ <td class="metric-name">Worst Offender</td>
+ <td class="metric"><?php echo $method . ' - ' . $count ?></td>
+ </tr>
+<?php endforeach ?>
+ <tr>
+ <th colspan="2">Class Averages</th>
+ </tr>
+<?php foreach (array_slice($data['class'], 0, 10) as $class => $count): ?>
+ <tr>
+ <td class="metric-name"><?php echo $class ?></td>
+ <td class="metric"><?php echo round($count, 2) ?></td>
+ </tr>
+<?php endforeach ?>
+</tbody></table>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/html/coverage.html.php b/libraries/lithium/test/templates/html/coverage.html.php
new file mode 100644
index 0000000..149b552
--- /dev/null
+++ b/libraries/lithium/test/templates/html/coverage.html.php
@@ -0,0 +1,79 @@
+<?php
+
+$summary = array(
+ 'classes' => 0, 'executable' => 0, 'covered' => 0, 'uncovered' => 0, 'percentage' => 0
+);
+
+?>
+
+<h3>Code Coverage</h3>
+<?php foreach ($data as $class => $coverage): ?>
+ <?php
+ $summary['classes']++;
+ $summary['executable'] += count($coverage['executable']);
+ $summary['covered'] += count($coverage['covered']);
+ $summary['uncovered'] += count($coverage['uncovered']);
+ $summary['percentage'] += $coverage['percentage'];
+ ?>
+ <h4 class="coverage">
+ <?php echo $class ?>:
+ <?php echo count($coverage['covered']) ?> of <?php echo count($coverage['executable']) ?>
+ lines covered (<em><?php echo $coverage['percentage'] ?>%</em>)
+ </h4>
+ <?php foreach ($coverage['output'] as $file => $data): ?>
+ <?php if (!empty($data)): ?>
+ <div class="code-coverage-results">
+ <?php foreach ($data as $line => $row): ?>
+ <div class="code-line <?php echo $row['class'] ?>">
+ <span class="line-num"><?php echo $line ?></span>
+ <span class="content"><?php
+ echo htmlspecialchars(str_replace("\t", " ", $row['data']))
+ ?></span>
+ </div><!-- code-line -->
+ <?php endforeach ?>
+ </div>
+ <h4 class="code-coverage-name"><?php echo $file ?></h4>
+ <!-- code-coverage-results -->
+ <?php endif ?>
+ <?php endforeach ?>
+<?php endforeach ?>
+
+<?php
+ if (!$summary['classes'] || !$summary['executable']) {
+ return;
+ }
+?>
+
+<br /><br />
+
+<h4>Summary</h4>
+<table class="metrics"><tbody>
+ <tr>
+ <td class="metric-name">Classes Covered</td>
+ <td class="metric"><?php echo $summary['classes'] ?></td>
+ </tr>
+ <tr>
+ <td class="metric-name">Executable Lines</td>
+ <td class="metric"><?php echo $summary['executable'] ?></td>
+ </tr>
+ <tr>
+ <td class="metric-name">Lines Covered</td>
+ <td class="metric"><?php echo $summary['covered'] ?></td>
+ </tr>
+ <tr>
+ <td class="metric-name">Lines Uncovered</td>
+ <td class="metric"><?php echo $summary['uncovered'] ?></td>
+ </tr>
+ <tr>
+ <td class="metric-name">Total Coverage</td>
+ <td class="metric">
+ <?php echo round(($summary['covered'] / $summary['executable']) * 100, 2) ?>%
+ </td>
+ </tr>
+ <tr>
+ <td class="metric-name">Average Per Class</td>
+ <td class="metric">
+ <?php echo round($summary['percentage'] / $summary['classes'], 2) ?>%
+ </td>
+ </tr>
+</tbody></table>
diff --git a/libraries/lithium/test/templates/html/exception.html.php b/libraries/lithium/test/templates/html/exception.html.php
new file mode 100644
index 0000000..6f72367
--- /dev/null
+++ b/libraries/lithium/test/templates/html/exception.html.php
@@ -0,0 +1,8 @@
+<div class="test-exception">
+ Exception thrown in <?php echo "{$error['class']}::{$error['method']}()"; ?>
+ on line <?php echo $error['line'] ?>:
+ <span class="content"><?php echo $error['message'] ?></span>
+ <?php if (isset($error['trace']) && !empty($error['trace'])): ?>
+ Trace: <span class="trace"><?php echo $error['trace'] ?></span>
+ <?php endif ?>
+</div>
diff --git a/libraries/lithium/test/templates/html/fail.html.php b/libraries/lithium/test/templates/html/fail.html.php
new file mode 100644
index 0000000..bbb8336
--- /dev/null
+++ b/libraries/lithium/test/templates/html/fail.html.php
@@ -0,0 +1,8 @@
+<div class="test-assert test-assert-failed">
+ Assertion '<?php echo $error['assertion'] ?>' failed in
+ <?php echo $error['class'] ?>::<?php echo $error['method']?>() on line
+ <?php echo $error['line'] ?>:
+ <span class="content"><?php echo htmlspecialchars(
+ $error['message'], ENT_QUOTES, 'UTF-8'
+ ) ?></span>
+</div>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/html/layout.html.php b/libraries/lithium/test/templates/html/layout.html.php
new file mode 100644
index 0000000..9354302
--- /dev/null
+++ b/libraries/lithium/test/templates/html/layout.html.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use lithium\util\Inflector;
+
+?>
+<!doctype html>
+<html>
+ <head>
+ <!-- Title intentionally left blank, forcing user agents use the current URL as title. -->
+ <title></title>
+ <?php $base = $request->env('base'); ?>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" href="<?php echo $base; ?>/css/debug.css" />
+ <link href="<?php echo $base; ?>/favicon.ico" type="image/x-icon" rel="icon" />
+ <link href="<?php echo $base; ?>/favicon.ico" type="image/x-icon" rel="shortcut icon" />
+ </head>
+ <body class="test-dashboard">
+ <div id="header">
+ <header>
+ <h1>
+ <a href="<?php echo $base ?>/test/">
+ <span class="triangle"></span> Lithium Unit Test Dashboard
+ </a>
+ </h1>
+ <a class="test-all" href="<?php echo $base ?>/test/all">run all tests</a>
+ </header>
+ </div>
+
+ <div class="article">
+ <article>
+ <div class="test-menu">
+ <?php echo $report->render("menu", array("menu" => $menu, "base" => $base)) ?>
+ </div>
+
+ <div class="test-content">
+ <?php if ($report->title) { ?>
+ <h2><span>test results for </span><?php echo $report->title; ?></h2>
+ <?php } ?>
+
+ <span class="filters">
+ <?php echo join('', array_map(
+ function($class) use ($request) {
+ $url = "?filters[]={$class}";
+ $name = join('', array_slice(explode("\\", $class), -1));
+ $key = Inflector::underscore($name);
+ $isActive = (
+ isset($request->query['filters']) &&
+ array_search($class, $request->query['filters']) !== false
+ );
+ $active = $isActive ? 'active' : null;
+ return "<a class=\"{$key} {$active}\" href=\"{$url}\">{$name}</a>";
+ },
+ $filters
+ )); ?>
+ </span>
+ <?php
+ echo $report->render("stats");
+
+ foreach ($report->filters() as $filter => $options) {
+ $data = $report->results['filters'][$filter];
+ echo $report->render($options['name'], compact('data', 'base'));
+ }
+ ?>
+ </div>
+ </article>
+ </div>
+ <div style="clear:both"></div>
+ </body>
+</html>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/html/menu.html.php b/libraries/lithium/test/templates/html/menu.html.php
new file mode 100644
index 0000000..c3013b4
--- /dev/null
+++ b/libraries/lithium/test/templates/html/menu.html.php
@@ -0,0 +1,58 @@
+<?php
+ $depth = 0;
+ $prev = array();
+ $current = null;
+?>
+<?php foreach ($menu as $test): ?>
+ <?php
+ $path = explode("\\", $test);
+ $case = array_pop($path);
+ $caseDepth = count($path);
+ for ($i = count($prev) - 1; $i > 0; $i--) {
+ if (isset($path[$i-1]) && $path[$i-1] != $prev[$i]) {
+ $caseDepth = $i;
+ $current = $prev[$i];
+ }
+ }
+ ?>
+ <?php if (!isset($current)): ?>
+ <ul class="menu">
+ <?php endif ?>
+
+ <?php while ($depth >= $caseDepth+1): ?>
+ </li></ul>
+ <?php
+ $depth--;
+ $current = array_pop($prev);
+ ?>
+ <?php endwhile ?>
+
+ <?php while(isset($current) && $current != $path[$depth-1]): ?>
+ </li></ul>
+ <?php
+ $current = array_pop($prev);
+ $depth--;
+ ?>
+ <?php endwhile ?>
+
+ <?php while ($depth < count($path)): ?>
+ <li>
+ <a class="menu-folder" title="run '<?php echo $path[$depth]; ?>' tests" href="<?php echo $base ?>/test/<?php echo join(array_slice($path, 0, $depth+1), "/") ?>">
+ <?php echo $path[$depth] ?>
+ </a>
+ <ul>
+ <?php
+ array_push($prev, $current);
+ $current = $path[$depth];
+ $depth++;
+ ?>
+ <?php endwhile ?>
+
+ <li><a href="<?php echo $base ?>/test/<?php echo join($path, "/") ?>/<?php echo $case ?>" title="run <?php echo $case; ?>"><?php echo $case ?></a></li>
+
+<?php endforeach ?>
+
+<?php while($depth > 0): ?>
+ </li></ul>
+ <?php $depth--; ?>
+<?php endwhile ?>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/html/profiler.html.php b/libraries/lithium/test/templates/html/profiler.html.php
new file mode 100644
index 0000000..ffe5dba
--- /dev/null
+++ b/libraries/lithium/test/templates/html/profiler.html.php
@@ -0,0 +1,10 @@
+<h3>Benchmarks</h3>
+<table class="metrics"><tbody>
+
+<?php foreach ($data['totals'] as $title => $result): ?>
+ <tr>
+ <td class="metric-name"><?php echo $title ?></td>
+ <td class="metric"><?php echo $result['formatter']($result['value']) ?></td>
+ </tr>
+<?php endforeach ?>
+</tbody></table>
\ No newline at end of file
diff --git a/libraries/lithium/test/templates/html/stats.html.php b/libraries/lithium/test/templates/html/stats.html.php
new file mode 100644
index 0000000..51f4ace
--- /dev/null
+++ b/libraries/lithium/test/templates/html/stats.html.php
@@ -0,0 +1,28 @@
+<?php
+
+$passes = intval($count['passes']) ?: 0;
+$asserts = intval($count['asserts']) ?: 0;
+$fails = intval($count['fails']) ?: 0;
+$exceptions = intval($count['exceptions']) ?: 0;
+
+?>
+<div class="test-result test-result-<?php echo ($success ? 'success' : 'fail') ?>">
+ <?php echo "{$passes} / {$asserts} passes, {$fails} " . ($fails == 1 ? ' fail' : ' fails'); ?>
+ and <?php echo $exceptions ?> <?php echo ($exceptions == 1 ? ' exception' : ' exceptions') ?>
+</div>
+
+<?php foreach ((array) $stats['errors'] as $error): ?>
+ <?php if ($error['result'] == 'fail' || $error['result'] == 'exception'): ?>
+ <?php echo $self->render("{$error['result']}", compact('error')); ?>
+ <?php endif ?>
+<?php endforeach ?>
+
+<?php foreach ((array) $stats['skips'] as $skip): ?>
+ <?php $trace = $skip['trace'][1]; ?>
+ <div class="test-skip">
+ <?php $method = $trace['function']; ?>
+ <?php $test = $trace['class'] . ($method != 'skip' ? "::{$method}()" : ''); ?>
+ Skipped test <?php echo $test ?>
+ <span class="content"><?php echo $skip['message'] ?></span>
+ </div>
+<?php endforeach ?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/action/ControllerTest.php b/libraries/lithium/tests/cases/action/ControllerTest.php
index 386161f..eb141da 100644
--- a/libraries/lithium/tests/cases/action/ControllerTest.php
+++ b/libraries/lithium/tests/cases/action/ControllerTest.php
@@ -2,17 +2,18 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\action;
-use \Exception;
-use \lithium\action\Request;
-use \lithium\action\Controller;
-use \lithium\tests\mocks\action\MockPostsController;
-use \lithium\tests\mocks\action\MockControllerRequest;
+use Exception;
+use lithium\net\http\Media;
+use lithium\action\Request;
+use lithium\action\Controller;
+use lithium\tests\mocks\action\MockPostsController;
+use lithium\tests\mocks\action\MockControllerRequest;
class ControllerTest extends \lithium\test\Unit {
@@ -40,24 +41,20 @@ class ControllerTest extends \lithium\test\Unit {
$result = $postsController->__invoke(null, array('action' => 'index', 'args' => array()));
$this->assertTrue(is_a($result, 'lithium\action\Response'));
- $this->assertEqual($result->body(), 'List of posts');
-
- $headers = array('Content-type' => 'text/plain');
- $this->assertEqual($result->headers, $headers);
+ $this->assertEqual('List of posts', $result->body());
+ $this->assertEqual(array('Content-type' => 'text/plain; charset=UTF-8'), $result->headers);
$result2 = $postsController(null, array('action' => 'index', 'args' => array()));
$this->assertEqual($result2, $result);
$postsController = new MockPostsController();
- $this->expectException('/Template not found/');
- $result = $postsController->__invoke(null, array(
- 'action' => 'index', 'args' => array(true)
- ));
+ $this->expectException('/Unhandled media type/');
+ $result = $postsController(null, array('action' => 'index', 'args' => array(true)));
$this->assertTrue(is_a($result, 'lithium\action\Response'));
$this->assertEqual($result->body, '');
- $headers = array('Content-type' => 'text/html');
+ $headers = array('Content-type' => 'text/html; charset=UTF-8');
$this->assertEqual($result->headers, $headers);
$result = $postsController->access('_render');
@@ -69,7 +66,7 @@ class ControllerTest extends \lithium\test\Unit {
$this->assertTrue(is_a($result, 'lithium\action\Response'));
$this->assertEqual($result->body, "Array\n(\n [0] => This is a post\n)\n");
- $headers = array('status' => 200, 'Content-type' => 'text/plain');
+ $headers = array('status' => 200, 'Content-type' => 'text/plain; charset=UTF-8');
$this->assertEqual($result->headers(), $headers);
$result = $postsController->access('_render');
@@ -85,7 +82,7 @@ class ControllerTest extends \lithium\test\Unit {
public function testRedirectResponse() {
$postsController = new MockPostsController();
- $result = $postsController->__invoke(null, array('action' => 'delete'));
+ $result = $postsController(null, array('action' => 'delete'));
$this->assertEqual($result->body(), '');
$headers = array('Location' => '/posts');
@@ -98,7 +95,7 @@ class ControllerTest extends \lithium\test\Unit {
$this->assertFalse($postsController->stopped);
$postsController = new MockPostsController(array('classes' => array(
- 'response' => '\lithium\tests\mocks\action\MockControllerResponse'
+ 'response' => 'lithium\tests\mocks\action\MockControllerResponse'
)));
$this->assertFalse($postsController->stopped);
@@ -123,7 +120,7 @@ class ControllerTest extends \lithium\test\Unit {
*/
public function testRenderWithAlternateTemplate() {
$postsController = new MockPostsController(array('classes' => array(
- 'media' => '\lithium\tests\mocks\action\MockMediaClass'
+ 'media' => 'lithium\tests\mocks\action\MockMediaClass'
)));
$result = $postsController(null, array('action' => 'view2'));
@@ -136,6 +133,47 @@ class ControllerTest extends \lithium\test\Unit {
}
/**
+ * Tests that requests where the controller class is specified manually continue to route to
+ * the correct template path.
+ *
+ * @return void
+ */
+ public function testRenderWithNamespacedController() {
+ $request = new Request();
+ $request->params['controller'] = 'lithium\tests\mocks\action\MockPostsController';
+
+ $controller = new MockPostsController(compact('request') + array('classes' => array(
+ 'media' => 'lithium\tests\mocks\action\MockMediaClass'
+ )));
+
+ $controller->render();
+ $this->assertEqual('mock_posts', $controller->response->options['controller']);
+ }
+
+ /**
+ * Verifies that data array is passed on to controller's response.
+ *
+ * @return void
+ */
+ public function testRenderWithDataArray() {
+ $request = new Request();
+ $request->params['controller'] = 'lithium\tests\mocks\action\MockPostsController';
+
+ $controller = new MockPostsController(compact('request') + array('classes' => array(
+ 'media' => 'lithium\tests\mocks\action\MockMediaClass'
+ )));
+
+ $controller->set(array('set' => 'data'));
+ $controller->render(array('data' => array('render' => 'data')));
+
+ $expected = array(
+ 'set' => 'data',
+ 'render' => 'data'
+ );
+ $this->assertEqual($expected, $controller->response->data);
+ }
+
+ /**
* Verifies that protected methods (i.e. prefixed with '_'), and methods declared in the
* Controller base class cannot be accessed.
*
@@ -143,7 +181,7 @@ class ControllerTest extends \lithium\test\Unit {
*/
public function testProtectedMethodAccessAttempt() {
$postsController = new MockPostsController();
- $this->expectException('/^Private/');
+ $this->expectException('/^Attempted to invoke a private method/');
$result = $postsController->__invoke(null, array('action' => 'redirect'));
$this->assertEqual($result->body, null);
@@ -159,11 +197,11 @@ class ControllerTest extends \lithium\test\Unit {
public function testResponseStatus() {
$postsController = new MockPostsController(array('classes' => array(
- 'response' => '\lithium\tests\mocks\action\MockControllerResponse'
+ 'response' => 'lithium\tests\mocks\action\MockControllerResponse'
)));
$this->assertFalse($postsController->stopped);
- $postsController->__invoke(null, array('action' => 'not_found'));
+ $postsController(null, array('action' => 'not_found'));
$result = $postsController->access('_render');
$this->assertTrue($result['hasRendered']);
@@ -176,32 +214,31 @@ class ControllerTest extends \lithium\test\Unit {
}
public function testResponseTypeBasedOnRequestType() {
- $request = new MockControllerRequest(array('type' => 'json'));
+ $request = new MockControllerRequest();
+ $request->params['type'] = 'json';
$postsController = new MockPostsController(array(
'request' => $request,
'classes' => array(
- 'response' => '\lithium\tests\mocks\action\MockControllerResponse'
+ 'response' => 'lithium\tests\mocks\action\MockControllerResponse'
)
));
$this->assertFalse($postsController->stopped);
- $postsController->__invoke($request, array('action' => 'type'));
+ $postsController($request, array('action' => 'type'));
$expected = array(
'type' => 'json', 'data' => array('data' => 'test'), 'auto' => true,
- 'layout' => 'default', 'template' => 'type', 'hasRendered' => true
+ 'layout' => 'default', 'template' => 'type', 'hasRendered' => true, 'negotiate' => false
);
$result = $postsController->access('_render');
$this->assertEqual($expected, $result);
- $expected = 'application/json';
$result = $postsController->response->headers('Content-type');
- $this->assertEqual($expected, $result);
+ $this->assertEqual('application/json; charset=UTF-8', $result);
- $expected = array('data' => 'test');
$result = json_decode($postsController->response->body(), true);
- $this->assertEqual($expected, $result);
+ $this->assertEqual(array('data' => 'test'), $result);
}
public function testResponseTypeBasedOnRequestParamsType() {
@@ -211,7 +248,7 @@ class ControllerTest extends \lithium\test\Unit {
$postsController = new MockPostsController(array(
'request' => $request,
'classes' => array(
- 'response' => '\lithium\tests\mocks\action\MockControllerResponse'
+ 'response' => 'lithium\tests\mocks\action\MockControllerResponse'
)
));
$this->assertFalse($postsController->stopped);
@@ -220,14 +257,13 @@ class ControllerTest extends \lithium\test\Unit {
$expected = array(
'type' => 'json', 'data' => array('data' => 'test'), 'auto' => true,
- 'layout' => 'default', 'template' => 'type', 'hasRendered' => true
+ 'layout' => 'default', 'template' => 'type', 'hasRendered' => true, 'negotiate' => false
);
$result = $postsController->access('_render');
$this->assertEqual($expected, $result);
- $expected = 'application/json';
$result = $postsController->response->headers('Content-type');
- $this->assertEqual($expected, $result);
+ $this->assertEqual('application/json; charset=UTF-8', $result);
$expected = array('data' => 'test');
$result = json_decode($postsController->response->body(), true);
@@ -241,43 +277,40 @@ class ControllerTest extends \lithium\test\Unit {
* @return void
*/
public function testManuallySettingTemplate() {
- $postsController = new MockControllerRequest();
+ $postsController = new MockPostsController(array('classes' => array(
+ 'media' => 'lithium\tests\mocks\action\MockMediaClass'
+ )));
$postsController(new Request(), array('action' => 'changeTemplate'));
$result = $postsController->access('_render');
$this->assertEqual('foo', $result['template']);
}
public function testResponseTypeBasedOnRequestHeaderType() {
- $request = new MockControllerRequest(array('env' => array('Content-type' => 'json')));
+ $request = new MockControllerRequest(array(
+ 'env' => array('HTTP_ACCEPT' => 'application/json,*/*')
+ ));
$postsController = new MockPostsController(array(
'request' => $request,
- 'classes' => array(
- 'response' => '\lithium\tests\mocks\action\MockControllerResponse'
- )
+ 'classes' => array('response' => 'lithium\tests\mocks\action\MockControllerResponse'),
+ 'render' => array('negotiate' => true)
));
$this->assertFalse($postsController->stopped);
- $postsController->__invoke($request, array('action' => 'type'));
+ $postsController($request, array('action' => 'type'));
$expected = array(
'type' => 'json', 'data' => array('data' => 'test'), 'auto' => true,
- 'layout' => 'default', 'template' => 'type', 'hasRendered' => true
+ 'layout' => 'default', 'template' => 'type', 'hasRendered' => true, 'negotiate' => true
);
$result = $postsController->access('_render');
$this->assertEqual($expected, $result);
- $expected = 'application/json';
$result = $postsController->response->headers('Content-type');
- $this->assertEqual($expected, $result);
+ $this->assertEqual('application/json; charset=UTF-8', $result);
- $expected = array('data' => 'test');
$result = json_decode($postsController->response->body(), true);
- $this->assertEqual($expected, $result);
- }
-
- public function methods() {
- return array('testDispatchingWithExplicitControllerName');
+ $this->assertEqual(array('data' => 'test'), $result);
}
/**
@@ -289,13 +322,19 @@ class ControllerTest extends \lithium\test\Unit {
public function testDispatchingWithExplicitControllerName() {
$request = new Request(array('url' => '/'));
$request->params = array(
- 'controller' => '\lithium\tests\mocks\action\MockPostsController',
+ 'controller' => 'lithium\tests\mocks\action\MockPostsController',
'action' => 'index'
);
$postsController = new MockPostsController(compact('request'));
$postsController->__invoke($request, $request->params);
}
+
+ public function testNonExistentFunction() {
+ $postsController = new MockPostsController();
+ $this->expectException("Action `foo` not found.");
+ $postsController(new Request(), array('action' => 'foo'));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/action/DispatcherTest.php b/libraries/lithium/tests/cases/action/DispatcherTest.php
index 37b8000..63f9f09 100644
--- a/libraries/lithium/tests/cases/action/DispatcherTest.php
+++ b/libraries/lithium/tests/cases/action/DispatcherTest.php
@@ -2,16 +2,17 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\action;
-use \lithium\action\Request;
-use \lithium\net\http\Router;
-use \lithium\action\Dispatcher;
-use \lithium\tests\mocks\action\MockDispatcher;
+use lithium\action\Request;
+use lithium\action\Response;
+use lithium\net\http\Router;
+use lithium\action\Dispatcher;
+use lithium\tests\mocks\action\MockDispatcher;
class DispatcherTest extends \lithium\test\Unit {
@@ -35,7 +36,7 @@ class DispatcherTest extends \lithium\test\Unit {
MockDispatcher::run(new Request(array('url' => '/')));
$result = end(MockDispatcher::$dispatched);
- $expected = array('controller' => 'test', 'action' => 'test');
+ $expected = array('controller' => 'Test', 'action' => 'test');
$this->assertEqual($expected, $result->params);
}
@@ -44,6 +45,55 @@ class DispatcherTest extends \lithium\test\Unit {
MockDispatcher::run(new Request(array('url' => '/')));
}
+ public function testApplyRulesControllerCasing() {
+ $params = array('controller' => 'test', 'action' => 'test');
+ $expected = array('controller' => 'Test', 'action' => 'test');
+ $this->assertEqual($expected, Dispatcher::applyRules($params));
+
+ $params = array('controller' => 'Test', 'action' => 'test');
+ $this->assertEqual($params, Dispatcher::applyRules($params));
+
+ $params = array('controller' => 'test_one', 'action' => 'test');
+ $expected = array('controller' => 'TestOne', 'action' => 'test');
+ $this->assertEqual($expected, Dispatcher::applyRules($params));
+ }
+
+ public function testApplyRulesWithNamespacedController() {
+ $params = array('controller' => 'li3_test\\Test', 'action' => 'test');
+ $expected = array('controller' => 'li3_test\\Test', 'action' => 'test');
+ $this->assertEqual($expected, Dispatcher::applyRules($params));
+ }
+
+ public function testApplyRulesDotNamespacing() {
+ $params = array('controller' => 'li3_test.test', 'action' => 'test');
+ $expected = array(
+ 'library' => 'li3_test', 'controller' => 'li3_test.Test', 'action' => 'test'
+ );
+ $this->assertEqual($expected, Dispatcher::applyRules($params));
+ }
+
+ public function testApplyRulesLibraryKeyNamespacing() {
+ $params = array('library' => 'li3_test', 'controller' => 'test', 'action' => 'test');
+ $expected = array(
+ 'library' => 'li3_test', 'controller' => 'li3_test.Test', 'action' => 'test'
+ );
+ $this->assertEqual($expected, Dispatcher::applyRules($params));
+ }
+
+ public function testApplyRulesNamespacingCollision() {
+ $params = array('library' => 'li3_one', 'controller' => 'li3_two.test', 'action' => 'test');
+ $expected = array(
+ 'library' => 'li3_one', 'controller' => 'li3_two.Test', 'action' => 'test'
+ );
+ $this->assertEqual($expected, Dispatcher::applyRules($params));
+
+ $params = array('library' => 'li3_one', 'controller' => 'li3_two\Test', 'action' => 'test');
+ $expected = array(
+ 'library' => 'li3_one', 'controller' => 'li3_two\Test', 'action' => 'test'
+ );
+ $this->assertEqual($expected, Dispatcher::applyRules($params));
+ }
+
public function testConfigManipulation() {
$config = MockDispatcher::config();
$expected = array('rules' => array());
@@ -57,31 +107,53 @@ class DispatcherTest extends \lithium\test\Unit {
MockDispatcher::run(new Request(array('url' => '/')));
$result = end(MockDispatcher::$dispatched);
- $expected = array('action' => 'admin_test', 'controller' => 'test', 'admin' => true);
+ $expected = array('action' => 'admin_test', 'controller' => 'Test', 'admin' => true);
$this->assertEqual($expected, $result->params);
}
public function testControllerLookupFail() {
Dispatcher::config(array('classes' => array('router' => __CLASS__)));
- $this->expectException('/Controller SomeNonExistentController not found/');
+ $this->expectException("/Controller `SomeNonExistentController` not found/");
Dispatcher::run(new Request(array('url' => '/')));
}
public function testPluginControllerLookupFail() {
Dispatcher::config(array('classes' => array('router' => __CLASS__)));
- $this->expectException('/Controller some_invalid_plugin.Controller not found/');
+ $this->expectException("/Controller `some_invalid_plugin.Controller` not found/");
Dispatcher::run(new Request(array('url' => '/plugin')));
}
- public static function parse($request) {
- switch ($request->url) {
- case '':
- return array('controller' => 'some_non_existent_controller', 'action' => 'index');
- case '/plugin':
- return array('controller' => 'some_invalid_plugin.controller', 'action' => 'index');
+ public function testCall() {
+ $result = MockDispatcher::run(new Request(array('url' => '/call')));
+ $this->assertEqual('Working', $result->body);
+ }
+
+ public function testAutoHandler() {
+ $result = MockDispatcher::run(new Request(array('url' => '/auto')));
+ $this->assertEqual(array('location: /redirect'), $result->headers());
+ }
+
+ public static function process($request) {
+ if ($request->url == '/auto') {
+ return new Response(array('location' => '/redirect'));
+ }
+
+ $params = array(
+ '' => array('controller' => 'some_non_existent_controller', 'action' => 'index'),
+ '/plugin' => array(
+ 'controller' => 'some_invalid_plugin.controller', 'action' => 'index'
+ ),
+ '/call' => array('action' => 'index', 'controller' => function($request) {
+ return new Response(array('body' => 'Working'));
+ })
+ );
+
+ if (isset($params[$request->url])) {
+ $request->params = $params[$request->url];
}
+ return $request;
}
}
diff --git a/libraries/lithium/tests/cases/action/RequestTest.php b/libraries/lithium/tests/cases/action/RequestTest.php
index c8458f2..51ac522 100644
--- a/libraries/lithium/tests/cases/action/RequestTest.php
+++ b/libraries/lithium/tests/cases/action/RequestTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\action;
-use \lithium\action\Request;
-use \lithium\tests\mocks\action\MockIisRequest;
-use \lithium\tests\mocks\action\MockCgiRequest;
+use lithium\action\Request;
+use lithium\tests\mocks\action\MockIisRequest;
+use lithium\tests\mocks\action\MockCgiRequest;
class RequestTest extends \lithium\test\Unit {
@@ -45,68 +45,49 @@ class RequestTest extends \lithium\test\Unit {
public function testInitMethodOverride() {
$_POST['Article']['title'] = 'cool';
- $_ENV['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'GET';
- $request = new Request();
-
- $expected = 'GET';
- $result = $request->env('REQUEST_METHOD');
- $this->assertEqual($expected, $result);
-
- $expected = array('Article' => array('title' => 'cool'));
- $result = $request->data;
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array('HTTP_X_HTTP_METHOD_OVERRIDE' => 'GET')));
- unset($_POST, $request);
+ $this->assertEqual('GET', $request->env('REQUEST_METHOD'));
+ $this->assertEqual(array('Article' => array('title' => 'cool')), $request->data);
+ unset($_POST);
}
public function testInitMethodOverrideWithEmptyServer() {
+ $_POST['Article']['title'] = 'cool';
$request = new Request(array('env' => array('HTTP_X_HTTP_METHOD_OVERRIDE' => 'POST')));
- $request->data = array('Article' => array('title' => 'cool'));
-
- $expected = 'POST';
- $result = $request->env('REQUEST_METHOD');
- $this->assertEqual($expected, $result);
-
- $expected = array('Article' => array('title' => 'cool'));
- $result = $request->data;
- $this->assertEqual($expected, $result);
-
- unset($_POST, $request);
+ $this->assertEqual('POST', $request->env('REQUEST_METHOD'));
+ $this->assertEqual(array('Article' => array('title' => 'cool')), $request->data);
+ unset($_POST['Article']);
}
public function testScriptFilename() {
$request = new Request(array('env' => array(
'SCRIPT_FILENAME' => '/lithium/app/webroot/index.php'
)));
-
- $expected = '/lithium/app/webroot/index.php';
$result = $request->env('SCRIPT_FILENAME');
- $this->assertEqual($expected, $result);
+ $this->assertEqual('/lithium/app/webroot/index.php', $result);
}
public function testPlatform() {
$request = new MockIisRequest();
-
- $expected = 'IIS';
$result = $request->env('PLATFORM');
- $this->assertEqual($expected, $result);
+ $this->assertEqual('IIS', $result);
}
public function testScriptFilenameTranslatedForIIS() {
$request = new MockIisRequest();
+ $this->assertEqual('\\lithium\\app\\webroot\\index.php', $request->env('SCRIPT_FILENAME'));
- $expected = '\\lithium\\app\\webroot\\index.php';
- $result = $request->env('SCRIPT_FILENAME');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array('SCRIPT_FILENAME' => null)));
+ $path = $request->env('DOCUMENT_ROOT') . $request->env('PHP_SELF');
+ $this->assertEqual($path, $request->env('SCRIPT_FILENAME'));
}
public function testDocumentRoot() {
- $_SERVER['DOCUMENT_ROOT'] = '/home/lithium/app/webroot';
- $request = new Request();
-
- $expected = '/home/lithium/app/webroot';
- $result = $request->env('DOCUMENT_ROOT');
- $this->assertEqual($expected, $result);
+ $request = new Request(array(
+ 'env' => array('DOCUMENT_ROOT' => '/home/lithium/app/webroot')
+ ));
+ $this->assertEqual('/home/lithium/app/webroot', $request->env('DOCUMENT_ROOT'));
}
public function testDocumentRootTranslatedForIIS() {
@@ -118,83 +99,57 @@ class RequestTest extends \lithium\test\Unit {
}
public function testScriptName() {
- $_SERVER['SCRIPT_NAME'] = 'index.php';
- $request = new Request();
-
- $expected = 'index.php';
- $result = $request->env('SCRIPT_NAME');
- $this->assertEqual($expected, $result);
+ $request = new Request(array(
+ 'env' => array('HTTPS' => true, 'SCRIPT_NAME' => 'index.php')
+ ));
+ $this->assertEqual('index.php', $request->env('SCRIPT_NAME'));
}
public function testHttps() {
- $_SERVER['HTTPS'] = true;
- $request = new Request();
-
- $expected = true;
- $result = $request->env('HTTPS');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array('HTTPS' => true)));
+ $this->assertTrue($request->env('HTTPS'));
}
public function testHttpsFromScriptUri() {
- $_SERVER['SCRIPT_URI'] = 'https://lithium.com';
- unset($_SERVER['HTTPS']);
- $request = new Request();
-
- $expected = true;
- $result = $request->env('HTTPS');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array(
+ 'SCRIPT_URI' => 'https://lithium.com',
+ 'HTTPS' => null
+ )));
+ $this->assertTrue($request->env('HTTPS'));
}
public function testRemoteAddr() {
- $_SERVER['REMOTE_ADDR'] = '123.456.789.000';
- $request = new Request();
-
- $expected = '123.456.789.000';
- $result = $request->env('REMOTE_ADDR');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array('REMOTE_ADDR' => '123.456.789.000')));
+ $this->assertEqual('123.456.789.000', $request->env('REMOTE_ADDR'));
}
public function testRemoteAddrFromHttpPcRemoteAddr() {
$request = new MockIisRequest();
-
- $expected = '123.456.789.000';
- $result = $request->env('REMOTE_ADDR');
- $this->assertEqual($expected, $result);
+ $this->assertEqual('123.456.789.000', $request->env('REMOTE_ADDR'));
}
public function testBase() {
- $_SERVER['PHP_SELF'] = '/index.php';
- $request = new Request();
-
- $expected = null;
- $result = $request->env('base');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array('PHP_SELF' => '/index.php')));
+ $this->assertFalse($request->env('base'));
}
public function testBaseWithDirectory() {
- $_SERVER['PHP_SELF'] = '/lithium.com/app/webroot/index.php';
- $request = new Request();
-
- $expected = '/lithium.com';
- $result = $request->env('base');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array(
+ 'PHP_SELF' => '/lithium.com/app/webroot/index.php'
+ )));
+ $this->assertEqual('/lithium.com', $request->env('base'));
}
public function testBaseWithAppAndOtherDirectory() {
- $_SERVER['PHP_SELF'] = '/lithium.com/app/other/webroot/index.php';
- $request = new Request();
-
- $expected = '/lithium.com/app/other';
- $result = $request->env('base');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array(
+ 'PHP_SELF' => '/lithium.com/app/other/webroot/index.php'
+ )));
+ $this->assertEqual('/lithium.com/app/other', $request->env('base'));
}
public function testPhpSelfTranslatedForIIS() {
$request = new MockIisRequest();
-
- $expected = '/index.php';
- $result = $request->env('PHP_SELF');
- $this->assertEqual($expected, $result);
+ $this->assertEqual('/index.php', $request->env('PHP_SELF'));
}
public function testServerHttpBase() {
@@ -209,9 +164,8 @@ class RequestTest extends \lithium\test\Unit {
public function testCgiPlatform() {
$request = new MockCgiRequest();
- $expected = true;
$result = $request->env('CGI_MODE');
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
}
public function testCgiScriptUrl() {
@@ -223,44 +177,52 @@ class RequestTest extends \lithium\test\Unit {
}
public function testGetMethod() {
- $_SERVER['PHP_SELF'] = '/lithium.com/app/webroot/index.php';
- $_POST['Article']['title'] = 'cool';
- $request = new Request();
+ $request = new Request(array('env' => array(
+ 'PHP_SELF' => '/lithium.com/app/webroot/index.php',
+ 'HTTP_ACCEPT' => 'text/html,application/xml,image/png,*/*',
+ 'HTTP_ACCEPT_LANGUAGE' => 'da, en-gb;q=0.8, en;q=0.7'
+ )));
+ $request->data = array('Article' => array('title' => 'cool'));
$expected = array('title' => 'cool');
$result = $request->get('data:Article');
$this->assertEqual($expected, $result);
- $expected = null;
$result = $request->get('not:Post');
- $this->assertEqual($expected, $result);
+ $this->assertNull($result);
$expected = '/lithium.com';
$result = $request->get('env:base');
$this->assertEqual($expected, $result);
- unset($_POST, $request);
+ $accept = $request->get('http:accept');
+ $this->assertEqual('text/html,application/xml,image/png,*/*', $accept);
+ $this->assertEqual($request->get('http:method'), $request->env('REQUEST_METHOD'));
}
public function testDetect() {
- $_SERVER['SOME_COOL_DETECTION'] = true;
- $request = new Request();
+ $request = new Request(array('env' => array('SOME_COOL_DETECTION' => true)));
$request->detect('cool', 'SOME_COOL_DETECTION');
- $expected = true;
- $result = $request->is('cool');
- $this->assertEqual($expected, $result);
+ $this->assertTrue($request->is('cool'));
+ $this->assertFalse($request->is('foo'));
+
+ $request = new Request(array('env' => array(
+ 'HTTP_USER_AGENT' => 'Mozilla/5.0 (iPhone; U; XXXXX like Mac OS X; en) AppleWebKit/420+'
+ )));
+
+ $request->detect('iPhone', array('HTTP_USER_AGENT', '/iPhone/'));
+ $isiPhone = $request->is('iPhone'); // returns true if 'iPhone' appears anywhere in the UA
+ $this->assertTrue($isiPhone);
}
public function testDetectWithClosure() {
$request = new Request();
- $request->detect('cool', function ($self) {
- return true;
- });
+ $request->detect('cool', function ($self) { return true; });
+ $request->detect('notCool', function ($self) { return false; });
- $expected = true;
- $result = $request->is('cool');
- $this->assertEqual($expected, $result);
+ $this->assertTrue($request->is('cool'));
+ $this->assertFalse($request->is('notCool'));
}
public function testDetectWithArray() {
@@ -269,36 +231,63 @@ class RequestTest extends \lithium\test\Unit {
return true;
}));
- $expected = true;
$result = $request->is('cool');
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
}
public function testDetectWithArrayRegex() {
- $_SERVER['SOME_COOL_DETECTION'] = 'this is cool';
- $request = new Request();
+ $request = new Request(array('env' => array('SOME_COOL_DETECTION' => 'this is cool')));
$request->detect('cool', array('SOME_COOL_DETECTION', '/cool/'));
- $expected = true;
$result = $request->is('cool');
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
+ }
+
+ public function testDetectSsl() {
+ $request = new Request(array('env' => array('SCRIPT_URI' => null, 'HTTPS' => 'off')));
+ $this->assertFalse($request->env('HTTPS'));
+
+ $request = new Request(array('env' => array('SCRIPT_URI' => null, 'HTTPS' => 'on')));
+ $this->assertTrue($request->env('HTTPS'));
+
+ $request = new Request(array('env' => array('SCRIPT_URI' => null, 'HTTPS' => null)));
+ $this->assertFalse($request->env('HTTPS'));
+ }
+
+ public function testContentTypeDetection() {
+ $request = new Request(array('env' => array(
+ 'CONTENT_TYPE' => 'application/json; charset=UTF-8',
+ 'REQUEST_METHOD' => 'POST'
+ )));
+ $this->assertTrue($request->is('json'));
+ $this->assertFalse($request->is('html'));
+ $this->assertFalse($request->is('foo'));
}
public function testIsMobile() {
- $_SERVER['HTTP_USER_AGENT'] = 'iPhone';
- $request = new Request();
+ $iPhone = 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like ';
+ $iPhone .= 'Gecko) Version/3.0 Mobile/1A535b Safari/419.3';
- $expected = true;
- $result = $request->is('mobile');
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array('HTTP_USER_AGENT' => $iPhone)));
+ $this->assertTrue($request->is('mobile'));
+
+ $android = 'Mozilla/5.0 (Linux; U; Android 0.5; en-us) AppleWebKit/522+ (KHTML, like ';
+ $android .= 'Gecko) Safari/419.3';
+
+ $request = new Request(array('env' => array('HTTP_USER_AGENT' => $android)));
+ $this->assertTrue($request->is('mobile'));
}
public function testType() {
$request = new Request();
+ $this->assertEqual('html', $request->type());
- $expected = 'html';
- $result = $request->type();
- $this->assertEqual($expected, $result);
+ $request = new Request(array('env' => array(
+ 'CONTENT_TYPE' => 'application/json; charset=UTF-8',
+ 'REQUEST_METHOD' => 'POST'
+ )));
+ $this->assertEqual('application/json; charset=UTF-8', $request->env('CONTENT_TYPE'));
+ $this->assertEqual('json', $request->type());
}
public function testRefererDefault() {
@@ -340,10 +329,11 @@ class RequestTest extends \lithium\test\Unit {
public function testMagicParamsAccess() {
$this->assertNull($this->request->action);
$this->assertFalse(isset($this->request->params['action']));
+ $this->assertFalse(isset($this->request->action));
$expected = $this->request->params['action'] = 'index';
- $result = $this->request->action;
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $this->request->action);
+ $this->assertTrue(isset($this->request->action));
}
public function testSingleFileNormalization() {
@@ -812,12 +802,158 @@ class RequestTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testAutomaticContentDecoding() {
+ $stream = fopen('php://temp', 'r+');
+ fwrite($stream, '{ "foo": "bar" }');
+ rewind($stream);
+ $request = new Request(compact('stream') + array('env' => array(
+ 'CONTENT_TYPE' => 'application/json; charset=UTF-8',
+ 'REQUEST_METHOD' => 'POST'
+ )));
+ $this->assertEqual(array('foo' => 'bar'), $request->data);
+ }
+
public function testRequestTypeFromHeader() {
- $request = new Request(array('env' => array('Content-type' => 'json')));
+ $request = new Request(array('env' => array('CONTENT_TYPE' => 'json')));
+ $this->assertEqual('json', $request->type());
+ }
- $expected = 'json';
- $result = $request->type();
- $this->assertEqual($expected, $result);
+ public function testResponseTypeDetection() {
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => 'text/xml,*/*')));
+ $this->assertEqual('xml', $request->accepts());
+
+ $request->params['type'] = 'json';
+ $this->assertEqual('json', $request->accepts());
+
+ $request = new Request(array('env' => array(
+ 'HTTP_ACCEPT' => 'application/xml,image/png,*/*'
+ )));
+ $this->assertEqual('xml', $request->accepts());
+
+ $request = new Request(array('env' => array(
+ 'HTTP_ACCEPT' => 'application/xml,application/xhtml+xml'
+ )));
+ $this->assertEqual('html', $request->accepts());
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => null)));
+ $this->assertEqual('html', $request->accepts());
+ }
+
+ /**
+ * Tests that accepted content-types without a `q` value are sorted in the order they appear in
+ * the `HTTP_ACCEPT` header.
+ */
+ public function testAcceptTypeOrder() {
+ $request = new Request(array('env' => array(
+ 'HTTP_ACCEPT' => 'application/xhtml+xml,text/html'
+ )));
+ $expected = array('application/xhtml+xml', 'text/html');
+ $this->assertEqual($expected, $request->accepts(true));
+
+ $request = new Request(array('env' => array(
+ 'HTTP_USER_AGENT' => 'Safari',
+ 'HTTP_ACCEPT' => 'application/xhtml+xml,text/html,text/plain;q=0.9'
+ )));
+ $expected = array('application/xhtml+xml', 'text/html', 'text/plain');
+ $this->assertEqual($expected, $request->accepts(true));
+ }
+
+ public function testParsingAcceptHeader() {
+ $chrome = array(
+ 'application/xml',
+ 'application/xhtml+xml',
+ 'text/html;q=0.9',
+ 'text/plain;q=0.8',
+ 'image/png',
+ '*/*;q=0.5'
+ );
+ $firefox = array(
+ 'text/html',
+ 'application/xhtml+xml',
+ 'application/xml;q=0.9',
+ '*/*;q=0.8'
+ );
+ $safari = array(
+ 'application/xml',
+ 'application/xhtml+xml',
+ 'text/html;q=0.9',
+ 'text/plain;q=0.8',
+ 'image/png',
+ '*/*;q=0.5'
+ );
+ $opera = array(
+ 'text/html',
+ 'application/xml;q=0.9',
+ 'application/xhtml+xml',
+ 'image/png',
+ 'image/jpeg',
+ 'image/gif',
+ 'image/x-xbitmap',
+ '*/*;q=0.1'
+ );
+ $android = array(
+ 'application/xml',
+ 'application/xhtml+xml',
+ 'text/html;q=0.9',
+ 'text/plain;q=0.8',
+ 'image/png',
+ '*/*;q=0.5',
+ 'application/youtube-client'
+ );
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => join(',', $chrome))));
+ $this->assertEqual('html', $request->accepts());
+ $this->assertTrue(array_search('text/plain', $request->accepts(true)), 4);
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => join(',', $safari))));
+ $this->assertEqual('html', $request->accepts());
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => join(',', $firefox))));
+ $this->assertEqual('html', $request->accepts());
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => join(',', $opera))));
+ $this->assertEqual('html', $request->accepts());
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => join(',', $chrome))));
+ $request->params['type'] = 'txt';
+
+ $result = $request->accepts(true);
+ $this->assertEqual('text/plain', $result[0]);
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => join(',', $android))));
+ $this->assertEqual('html', $request->accepts());
+ }
+
+ /**
+ * Tests that `Accept` headers with only one listed content type are parsed property, and tests
+ * that `'* /*'` is still parsed as `'text/html'`.
+ */
+ public function testAcceptSingleContentType() {
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => 'application/json,text/xml')));
+ $this->assertEqual(array('application/json', 'text/xml'), $request->accepts(true));
+ $this->assertEqual('json', $request->accepts());
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => 'application/json')));
+ $this->assertEqual(array('application/json'), $request->accepts(true));
+ $this->assertEqual('json', $request->accepts());
+
+ $request = new Request(array('env' => array('HTTP_ACCEPT' => '*/*')));
+ $this->assertEqual(array('text/html'), $request->accepts(true));
+ $this->assertEqual('html', $request->accepts());
+ }
+
+ /**
+ * Tests that `action\Request` correctly inherits the functionality of the `to()` method
+ * inherited from `lithium\net\http\Request`.
+ */
+ public function testConvertToUrl() {
+ $request = new Request(array(
+ 'env' => array('HTTP_HOST' => 'foo.com', 'HTTPS' => 'on'),
+ 'base' => '/the/base/path',
+ 'url' => '/the/url',
+ 'query' => array('some' => 'query', 'parameter' => 'values')
+ ));
+ $expected = 'https://foo.com/the/base/path/the/url?some=query¶meter=values';
+ $this->assertEqual($expected, $request->to('url'));
}
}
diff --git a/libraries/lithium/tests/cases/action/ResponseTest.php b/libraries/lithium/tests/cases/action/ResponseTest.php
index 3ae228a..39023ab 100644
--- a/libraries/lithium/tests/cases/action/ResponseTest.php
+++ b/libraries/lithium/tests/cases/action/ResponseTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\action;
-use \lithium\action\Response;
-use \lithium\tests\mocks\action\MockRequestType;
-use \lithium\tests\mocks\action\MockResponse;
+use lithium\action\Response;
+use lithium\tests\mocks\action\MockRequestType;
+use lithium\tests\mocks\action\MockResponse;
class ResponseTest extends \lithium\test\Unit {
@@ -20,13 +20,8 @@ class ResponseTest extends \lithium\test\Unit {
$this->response = new MockResponse(array('init' => false));
}
- public function testDefaultTypeInitialization() {
- $this->response = new Response(array('request' => new MockRequestType()));
- $this->assertEqual('foo', $this->response->type());
- }
-
public function testTypeManipulation() {
- $this->assertEqual('text/html', $this->response->type());
+ $this->assertEqual('html', $this->response->type());
$this->assertEqual('html', $this->response->type('html'));
$this->assertEqual('json', $this->response->type('json'));
$this->assertEqual('json', $this->response->type());
@@ -49,9 +44,35 @@ class ResponseTest extends \lithium\test\Unit {
$this->assertEqual('Document body', $result);
$this->assertEqual(array('HTTP/1.1 200 OK'), $this->response->testHeaders);
+ $expires = strtotime('+1 hour');
+ $this->response->cache($expires);
+ ob_start();
+ $this->response->render();
+ $result = ob_get_clean();
+ $headers = array (
+ 'HTTP/1.1 200 OK',
+ 'Expires: ' . gmdate('D, d M Y H:i:s', $expires) . ' GMT',
+ 'Cache-Control: max-age=' . ($expires - time()),
+ 'Pragma: cache'
+ );
+ $this->assertEqual($headers, $this->response->testHeaders);
+
+ $expires = '+2 hours';
+ $this->response->cache($expires);
+ ob_start();
+ $this->response->render();
+ $result = ob_get_clean();
+ $headers = array (
+ 'HTTP/1.1 200 OK',
+ 'Expires: ' . gmdate('D, d M Y H:i:s', strtotime($expires)) . ' GMT',
+ 'Cache-Control: max-age=' . (strtotime($expires) - time()),
+ 'Pragma: cache'
+ );
+ $this->assertEqual($headers, $this->response->testHeaders);
+
$this->response->body = 'Created';
$this->response->status(201);
- $this->response->disableCache();
+ $this->response->cache(false);
ob_start();
$this->response->render();
@@ -61,14 +82,17 @@ class ResponseTest extends \lithium\test\Unit {
$headers = array (
'HTTP/1.1 201 Created',
'Expires: Mon, 26 Jul 1997 05:00:00 GMT',
- 'Last-Modified: ' . gmdate("D, d M Y H:i:s") . ' GMT',
array(
'Cache-Control: no-store, no-cache, must-revalidate',
- 'Cache-Control: post-check=0, pre-check=0'
+ 'Cache-Control: post-check=0, pre-check=0',
+ 'Cache-Control: max-age=0'
),
'Pragma: no-cache'
);
$this->assertEqual($headers, $this->response->testHeaders);
+
+ $this->expectException('/^`Request::disableCache\(\)`.+`Request::cache\(false\)`/');
+ $this->response->disableCache();
}
/**
@@ -89,12 +113,12 @@ class ResponseTest extends \lithium\test\Unit {
$result = ob_get_clean();
$this->assertEqual(array('HTTP/1.1 303 See Other'), $this->response->testHeaders);
- $this->expectException('/Invalid status code/');
$this->response->status('foobar');
ob_start();
$this->response->render();
$result = ob_get_clean();
- $this->assertFalse($this->response->testHeaders);
+ $expected = array('HTTP/1.1 500 Internal Server Error');
+ $this->assertEqual($expected, $this->response->testHeaders);
}
/**
@@ -134,6 +158,18 @@ class ResponseTest extends \lithium\test\Unit {
$headers = array('HTTP/1.1 301 Moved Permanently', 'Location: /');
$this->assertEqual($headers, $this->response->testHeaders);
+
+ $this->response = new Response(array(
+ 'classes' => array('router' => __CLASS__),
+ 'location' => array('controller' => 'foo_bar', 'action' => 'index')
+ ));
+ $this->assertEqual(array('location: /foo_bar'), $this->response->headers());
+ }
+
+ public static function match($url) {
+ if ($url == array('controller' => 'foo_bar', 'action' => 'index')) {
+ return '/foo_bar';
+ }
}
}
diff --git a/libraries/lithium/tests/cases/analysis/DocblockTest.php b/libraries/lithium/tests/cases/analysis/DocblockTest.php
index dbb4134..f98848e 100644
--- a/libraries/lithium/tests/cases/analysis/DocblockTest.php
+++ b/libraries/lithium/tests/cases/analysis/DocblockTest.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\analysis;
-use \lithium\analysis\Docblock;
+use lithium\analysis\Docblock;
+use lithium\analysis\Inspector;
class DocblockTest extends \lithium\test\Unit {
@@ -22,11 +23,13 @@ class DocblockTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$comment = "/**\n * Lithium is cool\n * @foo bar\n * @baz qux\n */";
- $expected = array(
- 'description' => 'Lithium is cool',
- 'text' => '',
- 'tags' => array('foo' => 'bar', 'baz' => 'qux')
- );
+ $expected = array('description' => 'Lithium is cool', 'text' => '', 'tags' => array());
+ $result = Docblock::comment($comment);
+ $this->assertEqual($expected, $result);
+
+ Docblock::$tags[] = 'foo';
+ Docblock::$tags[] = 'baz';
+ $expected['tags'] = array('foo' => 'bar', 'baz' => 'qux');
$result = Docblock::comment($comment);
$this->assertEqual($expected, $result);
@@ -52,6 +55,66 @@ class DocblockTest extends \lithium\test\Unit {
$result = Docblock::comment($comment);
$this->assertEqual($expected, $result);
}
+
+ /**
+ * This is a short description.
+ *
+ * This is a longer description...
+ * That contains
+ * multiple lines
+ *
+ * @important This is a tag that spans a single line.
+ * @discuss This is a tag that
+ * spans
+ * several
+ * lines.
+ * @discuss The second discussion item
+ * @link http://example.com/
+ * @see lithium\analysis\Docblock
+ * @return void This tag contains a email@address.com.
+ */
+ public function testTagParsing() {
+ $info = Inspector::info(__METHOD__ . '()');
+ $result = Docblock::comment($info['comment']);
+ $this->assertEqual('This is a short description.', $result['description']);
+
+ $expected = "This is a longer description...\nThat contains\nmultiple lines";
+ $this->assertEqual($expected, $result['text']);
+
+ $tags = $result['tags'];
+ $expected = array('important', 'discuss', 'link', 'see', 'return');
+ $this->assertEqual($expected, array_keys($tags));
+
+ $this->assertEqual("This is a tag that\n spans\nseveral\nlines.", $tags['discuss'][0]);
+ $this->assertEqual("The second discussion item", $tags['discuss'][1]);
+
+ $this->assertEqual('void This tag contains a email@address.com.', $tags['return']);
+ $this->assertEqual(array(), Docblock::tags(null));
+
+ $this->assertEqual(array('params' => array()), Docblock::tags("Foobar\n\n@param string"));
+ }
+
+ public function testDocblockNewlineHandling() {
+ $doc = " * This line as well as the line below it,\r\n";
+ $doc .= " * are part of the description.\r\n *\r\n * This line isn't.";
+ $result = Docblock::comment($doc);
+
+ $description = "This line as well as the line below it,\nare part of the description.";
+ $this->assertEqual($description, $result['description']);
+
+ $this->assertEqual('This line isn\'t.', $result['text']);
+ }
+
+ /**
+ * This docblock has an extra * in the closing element.
+ *
+ */
+ public function testBadlyClosedDocblock() {
+ $info = Inspector::info(__METHOD__ . '()');
+ $description = 'This docblock has an extra * in the closing element.';
+ $this->assertEqual($description, $info['description']);
+ $this->assertEqual('', $info['text']);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/analysis/InspectorTest.php b/libraries/lithium/tests/cases/analysis/InspectorTest.php
index 5c0b44b..d1d45fd 100644
--- a/libraries/lithium/tests/cases/analysis/InspectorTest.php
+++ b/libraries/lithium/tests/cases/analysis/InspectorTest.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\analysis;
-use \ReflectionClass;
-use \ReflectionMethod;
-use \lithium\core\Libraries;
-use \lithium\analysis\Inspector;
+use ReflectionClass;
+use ReflectionMethod;
+use lithium\core\Libraries;
+use lithium\analysis\Inspector;
class InspectorTest extends \lithium\test\Unit {
@@ -39,7 +39,10 @@ class InspectorTest extends \lithium\test\Unit {
)));
$this->assertEqual($expected, $result);
- $result = Inspector::methods($class, 'ranges');
+ $this->assertNull(Inspector::methods('\lithium\core\Foo'));
+
+ $result = Inspector::methods('stdClass', 'extents');
+ $this->assertEqual(array(), $result);
}
public function testMethodInspection() {
@@ -182,18 +185,22 @@ class InspectorTest extends \lithium\test\Unit {
$this->assertEqual(array('modifiers', 'namespace'), array_keys($result));
$this->assertNull(Inspector::info('\lithium\analysis\Inspector::$foo'));
+
+ $this->assertNull(Inspector::info('\lithium\core\Foo::$foo'));
}
public function testClassDependencies() {
$expected = array(
- 'Exception', 'ReflectionClass', 'ReflectionException',
- 'lithium\\core\\Libraries', 'lithium\\util\\Collection'
+ 'Exception', 'ReflectionClass', 'ReflectionProperty', 'ReflectionException',
+ 'lithium\\core\\Libraries'
);
- $result = Inspector::dependencies($this->subject());
- $this->assertEqual($expected, $result);
$result = Inspector::dependencies($this->subject(), array('type' => 'static'));
$this->assertEqual($expected, $result);
+
+ $expected[] = 'lithium\\util\\Collection';
+ $result = Inspector::dependencies($this->subject());
+ $this->assertEqual($expected, $result);
}
/**
@@ -248,6 +255,26 @@ class InspectorTest extends \lithium\test\Unit {
)
);
$this->assertEqual($expected, $result);
+
+ $result = array_map(
+ function($property) { return $property['name']; },
+ Inspector::properties('lithium\action\Controller')
+ );
+ $this->assertTrue(in_array('request', $result));
+ $this->assertTrue(in_array('response', $result));
+ $this->assertFalse(in_array('_render', $result));
+ $this->assertFalse(in_array('_classes', $result));
+
+ $result = array_map(
+ function($property) { return $property['name']; },
+ Inspector::properties('lithium\action\Controller', array('public' => false))
+ );
+ $this->assertTrue(in_array('request', $result));
+ $this->assertTrue(in_array('response', $result));
+ $this->assertTrue(in_array('_render', $result));
+ $this->assertTrue(in_array('_classes', $result));
+
+ $this->assertNull(Inspector::properties('\lithium\core\Foo'));
}
}
diff --git a/libraries/lithium/tests/cases/analysis/LoggerTest.php b/libraries/lithium/tests/cases/analysis/LoggerTest.php
index 89e1b15..1f12e2e 100644
--- a/libraries/lithium/tests/cases/analysis/LoggerTest.php
+++ b/libraries/lithium/tests/cases/analysis/LoggerTest.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\analysis;
-use \lithium\analysis\Logger;
-use \lithium\util\Collection;
-use \lithium\tests\mocks\analysis\MockLoggerAdapter;
+use lithium\core\Libraries;
+use lithium\analysis\Logger;
+use lithium\util\Collection;
+use lithium\tests\mocks\analysis\MockLoggerAdapter;
/**
* Logger adapter test case
@@ -18,7 +19,7 @@ use \lithium\tests\mocks\analysis\MockLoggerAdapter;
class LoggerTest extends \lithium\test\Unit {
public function skip() {
- $this->_testPath = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not readable.");
}
@@ -32,15 +33,13 @@ class LoggerTest extends \lithium\test\Unit {
public function testConfig() {
$test = new MockLoggerAdapter();
- $config = array('logger' => array(
- 'adapter' => $test,
- 'filters' => array()
- ));
+ $config = array('logger' => array('adapter' => $test, 'filters' => array()));
$result = Logger::config($config);
$this->assertNull($result);
$result = Logger::config();
+ $config['logger'] += array('priority' => true);
$expected = $config;
$this->assertEqual($expected, $result);
}
@@ -57,36 +56,68 @@ class LoggerTest extends \lithium\test\Unit {
$result = Logger::config();
$this->assertFalse($result);
- $this->assertFalse(Logger::write('default', 'Test message.'));
+ $this->assertFalse(Logger::write('info', 'Test message.'));
}
public function testWrite() {
- $result = Logger::write('default', 'value');
+ $result = Logger::write('info', 'value');
$this->assertTrue($result);
}
public function testIntegrationWriteFile() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/logs';
+ $base = Libraries::get(true, 'resources') . '/tmp/logs';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
- $config = array('default' => array('adapter' => 'File'));
+ $config = array('default' => array(
+ 'adapter' => 'File', 'timestamp' => false, 'format' => "{:message}\n"
+ ));
Logger::config($config);
- $result = Logger::write('default', 'Message line 1');
- $this->assertTrue(file_exists($base . '/default.log'));
+ $result = Logger::write('info', 'Message line 1');
+ $this->assertTrue(file_exists($base . '/info.log'));
$expected = "Message line 1\n";
- $result = file_get_contents($base . '/default.log');
+ $result = file_get_contents($base . '/info.log');
$this->assertEqual($expected, $result);
- $result = Logger::write('default', 'Message line 2');
+ $result = Logger::write('info', 'Message line 2');
$this->assertTrue($result);
$expected = "Message line 1\nMessage line 2\n";
- $result = file_get_contents($base . '/default.log');
+ $result = file_get_contents($base . '/info.log');
+ $this->assertEqual($expected, $result);
+
+ unlink($base . '/info.log');
+ }
+
+ public function testWriteWithInvalidPriority() {
+ $this->expectException("Attempted to write log message with invalid priority `foo`.");
+ Logger::foo("Test message");
+ }
+
+ public function testWriteByName() {
+ $base = Libraries::get(true, 'resources') . '/tmp/logs';
+ $this->skipIf(!is_writable($base), "{$base} is not writable.");
+
+ Logger::config(array('default' => array(
+ 'adapter' => 'File',
+ 'timestamp' => false,
+ 'priority' => false,
+ 'format' => "{:message}\n"
+ )));
+
+ $this->assertFalse(file_exists($base . '/info.log'));
+
+ $this->assertFalse(Logger::write('info', 'Message line 1'));
+ $this->assertFalse(file_exists($base . '/info.log'));
+
+ $this->assertTrue(Logger::write(null, 'Message line 1', array('name' => 'default')));
+
+ $expected = "Message line 1\n";
+ $result = file_get_contents($base . '/.log');
$this->assertEqual($expected, $result);
- unlink($base . '/default.log');
+ unlink($base . '/.log');
}
}
diff --git a/libraries/lithium/tests/cases/analysis/ParserTest.php b/libraries/lithium/tests/cases/analysis/ParserTest.php
index 2901310..01814f6 100644
--- a/libraries/lithium/tests/cases/analysis/ParserTest.php
+++ b/libraries/lithium/tests/cases/analysis/ParserTest.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\analysis;
-use \lithium\analysis\Parser;
+use lithium\core\Libraries;
+use lithium\analysis\Parser;
class ParserTest extends \lithium\test\Unit {
@@ -106,7 +107,7 @@ class ParserTest extends \lithium\test\Unit {
}
public function testFindingTokenPatterns() {
- $code = file_get_contents(\lithium\core\Libraries::path('lithium\analysis\Parser'));
+ $code = file_get_contents(Libraries::path('lithium\analysis\Parser'));
$expected = array('tokenize', 'matchToken', '_prepareMatchParams', 'token');
$results = array_values(array_unique(array_map(function($i) { return $i[0]; }, Parser::find(
@@ -115,14 +116,7 @@ class ParserTest extends \lithium\test\Unit {
$this->assertEqual($expected, $results);
- $expected = array(
- '\ReflectionClass',
- '\lithium\core\Libraries',
- '\lithium\util\Collection',
- '\lithium\util\Validator',
- '\lithium\util\Set'
- );
-
+ $expected = array('ReflectionClass', 'lithium\util\Set', 'lithium\util\Collection');
$results = array_map(
function ($i) { return join('', $i); },
$results = Parser::find($code, 'use *;', array(
diff --git a/libraries/lithium/tests/cases/analysis/logger/adapter/FileTest.php b/libraries/lithium/tests/cases/analysis/logger/adapter/FileTest.php
index e11f72e..5980806 100644
--- a/libraries/lithium/tests/cases/analysis/logger/adapter/FileTest.php
+++ b/libraries/lithium/tests/cases/analysis/logger/adapter/FileTest.php
@@ -2,20 +2,60 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\analysis\logger\adapter;
-use \lithium\analysis\logger\adapter\File;
+use lithium\core\Libraries;
+use lithium\util\collection\Filters;
+use lithium\analysis\logger\adapter\File;
class FileTest extends \lithium\test\Unit {
+ public $subject;
+
+ public function skip() {
+ $path = realpath(Libraries::get(true, 'resources') . '/tmp/logs');
+ $this->skipIf(!is_writable($path), "Path `{$path}` is not writable.");
+ }
+
public function setUp() {
- die('WTF?');
- $this->path = LITHIUM_APP_PATH . '/resources/tmp/logs/';
- $this->Adapter = new File(array('path' => $this->path));
+ $this->path = Libraries::get(true, 'resources') . '/tmp/logs';
+ $this->tearDown();
+ }
+
+ public function tearDown() {
+ if (file_exists("{$this->path}/debug.log")) {
+ unlink("{$this->path}/debug.log");
+ }
+ }
+
+ public function testWriting() {
+ $this->subject = new File(array('path' => $this->path));
+ $priority = 'debug';
+ $message = 'This is a debug message';
+ $function = $this->subject->write($priority, $message);
+ $now = date('Y-m-d H:i:s');
+ $function('lithium\analysis\Logger', compact('priority', 'message'), new Filters());
+
+ $log = file_get_contents("{$this->path}/debug.log");
+ $this->assertEqual("{$now} This is a debug message\n", $log);
+ }
+
+ public function testWithoutTimestamp() {
+ $this->subject = new File(array(
+ 'path' => $this->path, 'timestamp' => false, 'format' => "{:message}\n"
+ ));
+ $priority = 'debug';
+ $message = 'This is a debug message';
+ $function = $this->subject->write($priority, $message);
+ $now = date('Y-m-d H:i:s');
+ $function('lithium\analysis\Logger', compact('priority', 'message'), new Filters());
+
+ $log = file_get_contents("{$this->path}/debug.log");
+ $this->assertEqual("This is a debug message\n", $log);
}
}
diff --git a/libraries/lithium/tests/cases/analysis/logger/adapter/GrowlTest.php b/libraries/lithium/tests/cases/analysis/logger/adapter/GrowlTest.php
new file mode 100644
index 0000000..8d3204c
--- /dev/null
+++ b/libraries/lithium/tests/cases/analysis/logger/adapter/GrowlTest.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\analysis\logger\adapter;
+
+use lithium\analysis\logger\adapter\Growl;
+
+class GrowlTest extends \lithium\test\Unit {
+
+ public function testGrowlWrite() {
+ $growl = new Growl(array(
+ 'name' => 'Lithium',
+ 'title' => 'Lithium log',
+ 'connection' => function() { return fopen('php://memory', 'w+'); }
+ ));
+ $writer = $growl->write('info', 'info: Test message.', array());
+ $params = array('message' => 'info: Test message.', 'options' => array());
+ $result = $writer('lithium\analysis\Logger', $params, null);
+
+ $bytes = array(
+ 1, 0, 0, 7, 1, 1, 76, 105, 116, 104, 105, 117, 109, 0, 6, 69, 114, 114, 111, 114, 115,
+ 0, 8, 77, 101, 115, 115, 97, 103, 101, 115, 0, 1, 186, 210, 71, 199, 54, 204, 63, 37,
+ 190, 184, 63, 79, 225, 46, 67, 23, 1, 1, 0, 0, 0, 8, 0, 11, 0, 19, 0, 7, 77, 101, 115,
+ 115, 97, 103, 101, 115, 76, 105, 116, 104, 105, 117, 109, 32, 108, 111, 103, 105, 110,
+ 102, 111, 58, 32, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101, 46, 76, 105,
+ 116, 104, 105, 117, 109, 213, 182, 8, 47, 80, 71, 225, 173, 12, 228, 108, 152, 140, 126,
+ 102, 14
+ );
+
+ rewind($growl->connection);
+ $result = array_map('ord', str_split(stream_get_contents($growl->connection)));
+ $this->assertEqual($bytes, $result);
+ }
+
+ public function testInvalidConnection() {
+ $growl = new Growl(array(
+ 'name' => 'Lithium',
+ 'title' => 'Lithium log',
+ 'port' => 0
+ ));
+ $this->expectException('/^Growl connection failed/');
+ $this->expectException('/Failed to parse address/');
+ $writer = $growl->write('info', 'info: Test message.', array());
+ }
+
+ public function testStickyMessages() {
+ $growl = new Growl(array(
+ 'name' => 'Lithium',
+ 'title' => 'Lithium log',
+ 'connection' => function() { return fopen('php://memory', 'w+'); }
+ ));
+ $writer = $growl->write('info', 'info: Test message.', array());
+ $params = array('message' => 'info: Test message.', 'options' => array('sticky' => true));
+ $result = $writer('lithium\analysis\Logger', $params, null);
+
+ $bytes = array(
+ 1, 0, 0, 7, 1, 1, 76, 105, 116, 104, 105, 117, 109, 0, 6, 69, 114, 114, 111, 114, 115,
+ 0, 8, 77, 101, 115, 115, 97, 103, 101, 115, 0, 1, 186, 210, 71, 199, 54, 204, 63, 37,
+ 190, 184, 63, 79, 225, 46, 67, 23, 1, 1, 1, 0, 0, 8, 0, 11, 0, 19, 0, 7, 77, 101, 115,
+ 115, 97, 103, 101, 115, 76, 105, 116, 104, 105, 117, 109, 32, 108, 111, 103, 105, 110,
+ 102, 111, 58, 32, 84, 101, 115, 116, 32, 109, 101, 115, 115, 97, 103, 101, 46, 76, 105,
+ 116, 104, 105, 117, 109, 123, 79, 163, 67, 106, 115, 6, 31, 170, 247, 50, 98, 144, 44,
+ 105, 89
+ );
+
+ rewind($growl->connection);
+ $result = array_map('ord', str_split(stream_get_contents($growl->connection)));
+ $this->assertEqual($bytes, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/analysis/logger/adapter/SyslogTest.php b/libraries/lithium/tests/cases/analysis/logger/adapter/SyslogTest.php
index 33110a2..f636a18 100644
--- a/libraries/lithium/tests/cases/analysis/logger/adapter/SyslogTest.php
+++ b/libraries/lithium/tests/cases/analysis/logger/adapter/SyslogTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\analysis\logger\adapter;
-use \lithium\analysis\Logger;
-use \lithium\analysis\logger\adapter\Syslog;
+use lithium\analysis\Logger;
+use lithium\analysis\logger\adapter\Syslog;
/**
* Syslog adapter test.
@@ -32,7 +32,6 @@ class SyslogTest extends \lithium\test\Unit {
'identity' => false,
'options' => LOG_ODELAY,
'facility' => LOG_USER,
- 'priority' => LOG_INFO,
'init' => true
);
$result = $this->syslog->_config;
@@ -54,7 +53,7 @@ class SyslogTest extends \lithium\test\Unit {
}
public function testWrite() {
- $result = Logger::write('syslog', 'SyslogTest message...');
+ $result = Logger::write('info', 'SyslogTest message...', array('name' => 'syslog'));
$this->assertTrue($result);
}
}
diff --git a/libraries/lithium/tests/cases/console/CommandTest.php b/libraries/lithium/tests/cases/console/CommandTest.php
index 7470b54..8cbb9d2 100644
--- a/libraries/lithium/tests/cases/console/CommandTest.php
+++ b/libraries/lithium/tests/cases/console/CommandTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console;
-use \lithium\console\Request;
-use \lithium\tests\mocks\console\MockCommand;
-use \lithium\tests\mocks\console\command\MockCommandHelp;
+use lithium\console\Request;
+use lithium\tests\mocks\console\MockCommand;
+use lithium\tests\mocks\console\command\MockCommandHelp;
class CommandTest extends \lithium\test\Unit {
@@ -18,7 +18,7 @@ class CommandTest extends \lithium\test\Unit {
public function setUp() {
$this->request = new Request(array('input' => fopen('php://temp', 'w+')));
- $this->classes = array('response' => '\lithium\tests\mocks\console\MockResponse');
+ $this->classes = array('response' => 'lithium\tests\mocks\console\MockResponse');
}
public function testConstruct() {
@@ -155,7 +155,8 @@ class CommandTest extends \lithium\test\Unit {
$this->assertTrue($return);
- $expected = "li3 MockCommand --case=CASE --face=FACE --mace=MACE --race=RACE -lace [ARGS]";
+ $expected = "li3 mock-command --case=CASE --face=FACE ";
+ $expected .= "--mace=MACE --race=RACE -lace [ARGS]";
$expected = preg_quote($expected);
$result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
@@ -169,11 +170,10 @@ class CommandTest extends \lithium\test\Unit {
}
public function testAdvancedHelp() {
- $this->request->params['command'] = 'mock_command_help';
$command = new MockCommandHelp(array('request' => $this->request));
$return = $command->__invoke('_help');
- $expected = "li3 MockCommandHelp --long=LONG -s [ARGS]";
+ $expected = "li3 mock-command-help --long=LONG -s [ARGS]";
$expected = preg_quote($expected);
$result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
diff --git a/libraries/lithium/tests/cases/console/CreateTest.php b/libraries/lithium/tests/cases/console/CreateTest.php
deleted file mode 100644
index a7cb0c7..0000000
--- a/libraries/lithium/tests/cases/console/CreateTest.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\console;
-
-class CreateTest extends \lithium\test\Unit {
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/DispatcherTest.php b/libraries/lithium/tests/cases/console/DispatcherTest.php
index 8659daa..86e73d5 100644
--- a/libraries/lithium/tests/cases/console/DispatcherTest.php
+++ b/libraries/lithium/tests/cases/console/DispatcherTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console;
-use \lithium\console\Dispatcher;
-use \lithium\console\Request;
+use lithium\console\Dispatcher;
+use lithium\console\Request;
class DispatcherTest extends \lithium\test\Unit {
@@ -26,14 +26,17 @@ class DispatcherTest extends \lithium\test\Unit {
public function testEmptyConfigReturnRules() {
$result = Dispatcher::config();
- $expected = array('rules' => array());
+ $expected = array('rules' => array(
+ 'command' => array(array('lithium\util\Inflector', 'camelize')),
+ 'action' => array(array('lithium\util\Inflector', 'camelize', array(false)))
+ ));
$this->assertEqual($expected, $result);
}
public function testConfigWithClasses() {
Dispatcher::config(array(
'classes' => array(
- 'request' => '\lithium\tests\mocks\console\MockDispatcherRequest'
+ 'request' => 'lithium\tests\mocks\console\MockDispatcherRequest'
)
));
$expected = 'run';
@@ -44,7 +47,7 @@ class DispatcherTest extends \lithium\test\Unit {
public function testRunWithCommand() {
$response = Dispatcher::run(new Request(array(
'args' => array(
- '\lithium\tests\mocks\console\MockDispatcherCommand'
+ 'lithium\tests\mocks\console\MockDispatcherCommand'
)
)));
$expected = 'run';
@@ -54,10 +57,7 @@ class DispatcherTest extends \lithium\test\Unit {
public function testRunWithPassed() {
$response = Dispatcher::run(new Request(array(
- 'args' => array(
- '\lithium\tests\mocks\console\MockDispatcherCommand',
- 'with param'
- )
+ 'args' => array('lithium\tests\mocks\console\MockDispatcherCommand', 'with param')
)));
$expected = 'run';
@@ -71,10 +71,7 @@ class DispatcherTest extends \lithium\test\Unit {
public function testRunWithAction() {
$response = Dispatcher::run(new Request(array(
- 'args' => array(
- '\lithium\tests\mocks\console\MockDispatcherCommand',
- 'testAction'
- )
+ 'args' => array('lithium\tests\mocks\console\MockDispatcherCommand', 'testAction')
)));
$expected = 'testAction';
$result = $response->testAction;
@@ -82,7 +79,7 @@ class DispatcherTest extends \lithium\test\Unit {
}
public function testInvalidCommand() {
- $expected = (object) array('status' => "Command `\\this\\command\\is\\fake` not found\n");
+ $expected = (object) array('status' => "Command `\\this\\command\\is\\fake` not found.\n");
$result = Dispatcher::run(new Request(array(
'args' => array(
'\this\command\is\fake',
@@ -92,6 +89,40 @@ class DispatcherTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+
+ public function testRunWithCamelizingCommand() {
+ $expected = (object) array('status' => "Command `FooBar` not found.\n");
+ $result = Dispatcher::run(new Request(array(
+ 'args' => array(
+ 'foo-bar',
+ )
+ )));
+ $this->assertEqual($expected, $result);
+
+ $expected = (object) array('status' => "Command `FooBar` not found.\n");
+ $result = Dispatcher::run(new Request(array(
+ 'args' => array('foo_bar')
+ )));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testRunWithCamelizingAction() {
+ $result = Dispatcher::run(new Request(array(
+ 'args' => array(
+ 'lithium\tests\mocks\console\command\MockCommandHelp',
+ 'sample-task-with-optional-args'
+ )
+ )));
+ $this->assertTrue($result);
+
+ $result = Dispatcher::run(new Request(array(
+ 'args' => array(
+ 'lithium\tests\mocks\console\command\MockCommandHelp',
+ 'sample_task_with_optional_args'
+ )
+ )));
+ $this->assertTrue($result);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/RequestTest.php b/libraries/lithium/tests/cases/console/RequestTest.php
index a3e5378..5188dcf 100644
--- a/libraries/lithium/tests/cases/console/RequestTest.php
+++ b/libraries/lithium/tests/cases/console/RequestTest.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console;
-use \lithium\console\Request;
+use lithium\core\Libraries;
+use lithium\console\Request;
class RequestTest extends \lithium\test\Unit {
@@ -18,7 +19,7 @@ class RequestTest extends \lithium\test\Unit {
public function setUp() {
$this->streams = array(
- 'input' => LITHIUM_APP_PATH . '/resources/tmp/tests/input.txt',
+ 'input' => Libraries::get(true, 'resources') . '/tmp/tests/input.txt'
);
$this->_backups['cwd'] = getcwd();
@@ -52,19 +53,19 @@ class RequestTest extends \lithium\test\Unit {
}
public function testEnvWorking() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_readable($base), "{$base} is not readable.");
- chdir(LITHIUM_APP_PATH . '/resources/tmp/tests');
+ chdir(Libraries::get(true, 'resources') . '/tmp/tests');
$request = new Request();
- $expected = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $expected = realpath(Libraries::get(true, 'resources') . '/tmp/tests');
$result = $request->env('working');
$this->assertEqual($expected, $result);
}
public function testConstructWithServer() {
- $_SERVER['argv'] = array('/path/to/lithium.php','one', 'two');
+ $_SERVER['argv'] = array('/path/to/lithium.php', 'one', 'two');
$request = new Request();
$expected = '/path/to/lithium.php';
@@ -72,30 +73,26 @@ class RequestTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$expected = array('one', 'two');
- $result = $request->args;
+ $result = $request->argv;
$this->assertEqual($expected, $result);
}
public function testConstructWithConfigArgv() {
- $request = new Request(array(
- 'args' => array('/path/to/lithium.php', 'wrong')
- ));
+ $request = new Request(array('args' => array('/path/to/lithium.php', 'wrong')));
$expected = array('/path/to/lithium.php', 'wrong');
- $result = $request->args;
+ $result = $request->argv;
$this->assertEqual($expected, $result);
$_SERVER['argv'] = array('/path/to/lithium.php');
- $request = new Request(array(
- 'args' => array('one', 'two')
- ));
+ $request = new Request(array('args' => array('one', 'two')));
$expected = '/path/to/lithium.php';
$result = $request->env('script');
$this->assertEqual($expected, $result);
$expected = array('one', 'two');
- $result = $request->args;
+ $result = $request->argv;
$this->assertEqual($expected, $result);
}
@@ -104,7 +101,7 @@ class RequestTest extends \lithium\test\Unit {
'args' => array('ok')
));
$expected = array('ok');
- $this->assertEqual($expected, $request->args);
+ $this->assertEqual($expected, $request->argv);
$request = new Request(array(
'env' => array('script' => '/path/to/lithium.php'),
@@ -116,17 +113,15 @@ class RequestTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$expected = array('one', 'two', 'three', 'four');
- $this->assertEqual($expected, $request->args);
+ $this->assertEqual($expected, $request->argv);
}
public function testConstructWithEnv() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_readable($base), "{$base} is not writable.");
- chdir(LITHIUM_APP_PATH . '/resources/tmp');
- $request = new Request(array(
- 'env' => array('working' => '/some/other/path')
- ));
+ chdir(Libraries::get(true, 'resources') . '/tmp');
+ $request = new Request(array('env' => array('working' => '/some/other/path')));
$expected = '/some/other/path';
$result = $request->env('working');
@@ -134,25 +129,26 @@ class RequestTest extends \lithium\test\Unit {
}
public function testInput() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
$stream = fopen($this->streams['input'], 'w+');
- $request = new Request(array(
- 'input' => $stream
- ));
+ $request = new Request(array('input' => $stream));
$this->assertTrue(is_resource($request->input));
$this->assertEqual($stream, $request->input);
-
- $expected = 2;
- $result = fwrite($request->input, 'ok');
- $this->assertEqual($expected, $result);
+ $this->assertEqual(2, fwrite($request->input, 'ok'));
rewind($request->input);
- $expected = 'ok';
- $result = $request->input();
- $this->assertEqual($expected, $result);
+ $this->assertEqual('ok', $request->input());
+ }
+
+ public function testArgs() {
+ $request = new Request();
+ $request->params = array(
+ 'command' => 'one', 'action' => 'two', 'args' => array('three', 'four', 'five')
+ );
+ $this->assertEqual('five', $request->args(2));
}
public function testShiftDefaultOne() {
@@ -164,8 +160,7 @@ class RequestTest extends \lithium\test\Unit {
$request->shift();
$expected = array('command' => 'two', 'action' => 'three', 'args' => array('four', 'five'));
- $result = $request->params;
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $request->params);
}
public function testShiftTwo() {
diff --git a/libraries/lithium/tests/cases/console/ResponseTest.php b/libraries/lithium/tests/cases/console/ResponseTest.php
index a7e15a1..572764b 100644
--- a/libraries/lithium/tests/cases/console/ResponseTest.php
+++ b/libraries/lithium/tests/cases/console/ResponseTest.php
@@ -2,14 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console;
-use \lithium\console\Response;
-use \lithium\console\Request;
+use lithium\core\Libraries;
+use lithium\console\Response;
+use lithium\console\Request;
class ResponseTest extends \lithium\test\Unit {
@@ -17,8 +18,8 @@ class ResponseTest extends \lithium\test\Unit {
public function setUp() {
$this->streams = array(
- 'output' => LITHIUM_APP_PATH . '/resources/tmp/tests/output.txt',
- 'error' => LITHIUM_APP_PATH . '/resources/tmp/tests/error.txt'
+ 'output' => Libraries::get(true, 'resources') . '/tmp/tests/output.txt',
+ 'error' => Libraries::get(true, 'resources') . '/tmp/tests/error.txt'
);
}
@@ -33,15 +34,14 @@ class ResponseTest extends \lithium\test\Unit {
public function testConstructWithoutConfig() {
$response = new Response();
$this->assertTrue(is_resource($response->output));
-
$this->assertTrue(is_resource($response->error));
}
public function testConstructWithConfigOutput() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
-
$stream = fopen($this->streams['output'], 'w');
+
$response = new Response(array(
'output' => $stream
));
@@ -50,53 +50,36 @@ class ResponseTest extends \lithium\test\Unit {
}
- public function testConstructWithConfigErrror() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ public function testConstructWithConfigError() {
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
$stream = fopen($this->streams['error'], 'w');
- $response = new Response(array(
- 'error' => $stream
- ));
+ $response = new Response(array('error' => $stream));
$this->assertTrue(is_resource($response->error));
$this->assertEqual($stream, $response->error);
-
}
public function testOutput() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
- $response = new Response(array(
- 'output' => fopen($this->streams['output'], 'w+')
- ));
+ $response = new Response(array('output' => fopen($this->streams['output'], 'w+')));
$this->assertTrue(is_resource($response->output));
- $expected = 2;
- $result = $response->output('ok');
- $this->assertEqual($expected, $result);
- $expected = 'ok';
- $result = file_get_contents($this->streams['output']);
- $this->assertEqual($expected, $result);
+ $this->assertEqual(2, $response->output('ok'));
+ $this->assertEqual('ok', file_get_contents($this->streams['output']));
}
public function testError() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
- $response = new Response(array(
- 'error' => fopen($this->streams['error'], 'w+')
- ));
+ $response = new Response(array('error' => fopen($this->streams['error'], 'w+')));
$this->assertTrue(is_resource($response->error));
-
- $expected = 2;
- $result = $response->error('ok');
- $this->assertEqual($expected, $result);
-
- $expected = 'ok';
- $result = file_get_contents($this->streams['error']);
- $this->assertEqual($expected, $result);
+ $this->assertEqual(2, $response->error('ok'));
+ $this->assertEqual('ok', file_get_contents($this->streams['error']));
}
}
diff --git a/libraries/lithium/tests/cases/console/RouterTest.php b/libraries/lithium/tests/cases/console/RouterTest.php
index fcde1f6..7a8dced 100644
--- a/libraries/lithium/tests/cases/console/RouterTest.php
+++ b/libraries/lithium/tests/cases/console/RouterTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console;
-use \lithium\console\Router;
-use \lithium\console\Request;
+use lithium\console\Router;
+use lithium\console\Request;
class RouterTest extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/console/command/CreateTest.php b/libraries/lithium/tests/cases/console/command/CreateTest.php
index 191bb8c..cd35356 100644
--- a/libraries/lithium/tests/cases/console/command/CreateTest.php
+++ b/libraries/lithium/tests/cases/console/command/CreateTest.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console\command;
-use \lithium\tests\mocks\console\command\MockCreate;
-use \lithium\console\Request;
-use \lithium\core\Libraries;
+use lithium\tests\mocks\console\command\MockCreate;
+use lithium\console\Request;
+use lithium\core\Libraries;
+use lithium\data\Connections;
class CreateTest extends \lithium\test\Unit {
@@ -21,7 +22,7 @@ class CreateTest extends \lithium\test\Unit {
protected $_testPath = null;
public function skip() {
- $this->_testPath = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not writable.");
}
@@ -35,7 +36,12 @@ class CreateTest extends \lithium\test\Unit {
Libraries::add('app', array('path' => $this->_testPath . '/new', 'bootstrap' => false));
Libraries::add('create_test', array('path' => $this->_testPath . '/create_test'));
$this->request = new Request(array('input' => fopen('php://temp', 'w+')));
- $this->request->params = array('library' => 'create_test');
+ $this->request->params = array('library' => 'create_test', 'action' => null);
+
+ Connections::add('default', array(
+ 'type' => null,
+ 'adapter' => 'lithium\tests\mocks\data\model\MockDatabase'
+ ));
}
public function tearDown() {
@@ -53,29 +59,36 @@ class CreateTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
- public function testSaveWithApp() {
-
- chdir($this->_testPath);
- $this->request->params = array('library' => 'app');
+ public function testNonExistentCommand() {
+ $this->request->params['args'] = array('does_not_exist', 'anywhere');
$create = new MockCreate(array('request' => $this->request));
- $result = $create->save('test', array(
- 'namespace' => 'app\tests\cases\models',
- 'use' => 'app\models\Post',
- 'class' => 'PostTest',
- 'methods' => "\tpublic function testCreate() {\n\n\t}\n",
- ));
- $this->assertTrue($result);
- $result = $this->_testPath . '/new/tests/cases/models/PostTest.php';
- $this->assertTrue(file_exists($result));
+ $result = $create->run('does_not_exist');
+ $this->assertFalse($result);
- $this->_cleanUp();
+ $expected = "does_not_exist could not be created.\n";
+ $result = $create->response->error;
+ $this->assertEqual($expected, $result);
}
- public function testSaveWithLibrary() {
+ public function testNamespace() {
+ $create = new MockCreate(array('request' => $this->request));
+ $this->request->params['command'] = 'one';
+
+ $expected = 'create_test\\two';
+ $result = $create->invokeMethod('_namespace', array(
+ $this->request, array(
+ 'spaces' => array('one' => 'two')
+ )
+ ));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSave() {
chdir($this->_testPath);
+ $this->request->params = array('library' => 'create_test', 'template' => 'test');
$create = new MockCreate(array('request' => $this->request));
- $result = $create->save('test', array(
+ $result = $create->save(array(
'namespace' => 'create_test\tests\cases\models',
'use' => 'create_test\models\Post',
'class' => 'PostTest',
@@ -92,21 +105,41 @@ class CreateTest extends \lithium\test\Unit {
public function testRunWithoutCommand() {
$create = new MockCreate(array('request' => $this->request));
- $expected = null;
$result = $create->run();
+ $this->assertFalse($result);
+
+ $expected = "What would you like to create? (model/view/controller/test/mock) \n > ";
+ $result = $create->response->output;
$this->assertEqual($expected, $result);
}
- public function testRunWithModelCommand() {
+ public function testRunNotSaved() {
+ $this->request->params = array(
+ 'library' => 'not_here', 'command' => 'create', 'action' => 'model',
+ 'args' => array('model', 'Post')
+ );
$create = new MockCreate(array('request' => $this->request));
- $this->request->params += array(
- 'command' => 'create', 'action' => 'run', 'args' => array('model')
+ $result = $create->run('model');
+ $this->assertFalse($result);
+
+ $expected = "model could not be created.\n";
+ $result = $create->response->error;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testRunWithModelCommand() {
+ $this->request->params = array(
+ 'library' => 'create_test', 'command' => 'create', 'action' => 'model',
+ 'args' => array('Post')
);
- $create->run('model', 'Post');
+
+ $create = new MockCreate(array('request' => $this->request));
+
+ $create->run('model');
$expected = 'model';
- $result = $create->request->params['command'];
+ $result = $create->request->command;
$this->assertEqual($expected, $result);
$result = $this->_testPath . '/create_test/models/Post.php';
@@ -115,38 +148,78 @@ class CreateTest extends \lithium\test\Unit {
public function testRunWithTestModelCommand() {
$this->request->params = array(
- 'command' => 'create', 'action' => 'run',
- 'args' => array('test', 'model', 'Post'),
- 'library' => 'create_test'
+ 'library' => 'create_test', 'command' => 'create', 'action' => 'test',
+ 'args' => array('model', 'Post'),
);
+
$create = new MockCreate(array('request' => $this->request));
- $create->run('test', 'model');
+ $create->run('test');
- $expected = 'test';
- $result = $create->request->params['command'];
+ $expected = 'model';
+ $result = $create->request->command;
$this->assertEqual($expected, $result);
$result = $this->_testPath . '/create_test/tests/cases/models/PostTest.php';
$this->assertTrue(file_exists($result));
}
- public function testRunWithTestOtherCommand() {
+ public function testRunWithTestControllerCommand() {
+ $this->request->params = array(
+ 'library' => 'create_test', 'command' => 'create', 'action' => 'test',
+ 'args' => array('controller', 'Post'),
+ );
+
$create = new MockCreate(array('request' => $this->request));
+
+ $create->run('test');
+
+ $expected = 'controller';
+ $result = $create->request->command;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_testPath . '/create_test/tests/cases/controllers/PostsControllerTest.php';
+ $this->assertTrue(file_exists($result));
+ }
+
+ public function testRunWithTestOtherCommand() {
$this->request->params = array(
- 'command' => 'create', 'action' => 'run',
- 'args' => array('test', 'something', 'Post'),
- 'library' => 'create_test'
+ 'library' => 'create_test', 'command' => 'create', 'action' => 'test',
+ 'args' => array('something', 'Post'),
);
- $create->run('test', 'something');
- $expected = 'test';
- $result = $create->request->params['command'];
+ $create = new MockCreate(array('request' => $this->request));
+ $create->run('test');
+
+ $expected = 'something';
+ $result = $create->request->command;
$this->assertEqual($expected, $result);
$result = $this->_testPath . '/create_test/tests/cases/something/PostTest.php';
$this->assertTrue(file_exists($result));
}
+
+ public function testRunAll() {
+ $this->request->params = array(
+ 'library' => 'create_test', 'command' => 'create', 'action' => 'Post',
+ 'args' => array(),
+ );
+
+ $create = new MockCreate(array('request' => $this->request));
+ $create->run('Post');
+
+ $result = $this->_testPath . '/create_test/models/Post.php';
+ $this->assertTrue(file_exists($result));
+
+ $result = $this->_testPath . '/create_test/controllers/PostsController.php';
+ $this->assertTrue(file_exists($result));
+
+ $result = $this->_testPath . '/create_test/tests/cases/models/PostTest.php';
+ $this->assertTrue(file_exists($result));
+
+ $result = $this->_testPath . '/create_test/tests/cases/controllers/PostsControllerTest.php';
+ $this->assertTrue(file_exists($result));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/command/HelpTest.php b/libraries/lithium/tests/cases/console/command/HelpTest.php
index 4d18794..5fe4777 100644
--- a/libraries/lithium/tests/cases/console/command/HelpTest.php
+++ b/libraries/lithium/tests/cases/console/command/HelpTest.php
@@ -2,9 +2,9 @@
namespace lithium\tests\cases\console\command;
-use \lithium\console\command\Help;
-use \lithium\console\Request;
-use \lithium\tests\mocks\console\command\MockCommandHelp;
+use lithium\console\command\Help;
+use lithium\console\Request;
+use lithium\tests\mocks\console\command\MockCommandHelp;
class HelpTest extends \lithium\test\Unit {
@@ -12,10 +12,8 @@ class HelpTest extends \lithium\test\Unit {
protected $_backup = array();
- protected $_testPath = null;
-
public function setUp() {
- $this->classes = array('response' => '\lithium\tests\mocks\console\MockResponse');
+ $this->classes = array('response' => 'lithium\tests\mocks\console\MockResponse');
$this->_backup['cwd'] = getcwd();
$this->_backup['_SERVER'] = $_SERVER;
$_SERVER['argv'] = array();
@@ -30,116 +28,105 @@ class HelpTest extends \lithium\test\Unit {
}
public function testRun() {
- $help = new Help(array(
- 'request' => $this->request, 'classes' => $this->classes
- ));
- $expected = true;
- $result = $help->run();
- $this->assertEqual($expected, $result);
+ $command = new Help(array('request' => $this->request, 'classes' => $this->classes));
+ $this->assertTrue($command->run());
$expected = "COMMANDS\n";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
- $expected = "Test\n Runs a given set of tests and outputs the results.";
$expected = preg_quote($expected);
- $result = $help->response->output;
- $this->assertPattern("/{$expected}/", $result);
-
+ $result = $command->response->output;
+ $pattern = "/\s+test\s+Runs a given set of tests and outputs the results\./ms";
+ $this->assertPattern($pattern, $result);
}
public function testRunWithName() {
- $help = new Help(array(
+ $command = new Help(array(
'request' => $this->request, 'classes' => $this->classes
));
- $expected = true;
- $result = $help->run('Test');
- $this->assertEqual($expected, $result);
+ $result = $command->run('Test');
+ $this->assertTrue($result);
- $expected = "li3 Test --case=CASE --group=GROUP --filters=FILTERS [ARGS]";
+ $expected = "li3 test --case=CASE --group=GROUP --filters=FILTERS [ARGS]";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
$expected = "OPTIONS\n --case=CASE\n";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
$expected = "missing\n";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
}
public function testApiClass() {
- $help = new Help(array(
+ $command = new Help(array(
'request' => $this->request, 'classes' => $this->classes
));
- $expected = null;
- $result = $help->api('lithium.util.Inflector');
- $this->assertEqual($expected, $result);
+ $result = $command->api('lithium.util.Inflector');
+ $this->assertNull($result);
$expected = "Utility for modifying format of words";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
}
public function testApiMethod() {
- $help = new Help(array(
+ $command = new Help(array(
'request' => $this->request, 'classes' => $this->classes
));
- $expected = null;
- $result = $help->api('lithium.util.Inflector', 'method');
- $this->assertEqual($expected, $result);
+ $result = $command->api('lithium.util.Inflector', 'method');
+ $this->assertNull($result);
$expected = "rules [type] [config]";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
}
public function testApiMethodWithName() {
- $help = new Help(array(
+ $command = new Help(array(
'request' => $this->request, 'classes' => $this->classes
));
- $expected = null;
- $result = $help->api('lithium.util.Inflector', 'method', 'rules');
- $this->assertEqual($expected, $result);
+ $result = $command->api('lithium.util.Inflector', 'method', 'rules');
+ $this->assertNull($result);
$expected = "rules [type] [config]";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
}
public function testApiProperty() {
- $help = new Help(array(
+ $command = new Help(array(
'request' => $this->request, 'classes' => $this->classes
));
- $expected = null;
- $result = $help->api('lithium.net.http.Request', 'property');
- $this->assertEqual($expected, $result);
+ $result = $command->api('lithium.net.Message', 'property');
+ $this->assertNull($result);
- $expected = " --host=HOST\n The Host header value and authority.";
+ $expected = " --host=HOST\n The hostname for this endpoint.";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
}
public function testApiPropertyWithName() {
- $help = new Help(array(
+ $command = new Help(array(
'request' => $this->request, 'classes' => $this->classes
));
- $expected = null;
- $result = $help->api( 'lithium.net.http.Request', 'property', 'host');
- $this->assertEqual($expected, $result);
+ $result = $command->api('lithium.net.Message', 'property');
+ $this->assertNull($result);
- $expected = " --host=HOST\n The Host header value and authority.";
+ $expected = " --host=HOST\n The hostname for this endpoint.";
$expected = preg_quote($expected);
- $result = $help->response->output;
+ $result = $command->response->output;
$this->assertPattern("/{$expected}/", $result);
}
}
diff --git a/libraries/lithium/tests/cases/console/command/LibraryTest.php b/libraries/lithium/tests/cases/console/command/LibraryTest.php
index 4b728c8..026163a 100644
--- a/libraries/lithium/tests/cases/console/command/LibraryTest.php
+++ b/libraries/lithium/tests/cases/console/command/LibraryTest.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console\command;
-use \Phar;
-use \lithium\console\command\Library;
-use \lithium\core\Libraries;
-use \lithium\console\Request;
+use Phar;
+use lithium\console\command\Library;
+use lithium\core\Libraries;
+use lithium\console\Request;
class LibraryTest extends \lithium\test\Unit {
@@ -22,7 +22,7 @@ class LibraryTest extends \lithium\test\Unit {
protected $_testPath = null;
public function skip() {
- $this->_testPath = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not writable.");
}
@@ -42,14 +42,14 @@ class LibraryTest extends \lithium\test\Unit {
));
$this->classes = array(
- 'service' => '\lithium\tests\mocks\console\command\MockLibraryService',
- 'response' => '\lithium\tests\mocks\console\MockResponse'
+ 'service' => 'lithium\tests\mocks\console\command\MockLibraryService',
+ 'response' => 'lithium\tests\mocks\console\MockResponse'
);
$this->request = new Request(array('input' => fopen('php://temp', 'w+')));
$this->library = new Library(array(
'request' => $this->request, 'classes' => $this->classes
));
- $this->library->conf = $this->_conf = $this->_testPath . '/library.json';
+ $this->testConf = $this->library->conf = $this->_testPath . '/library.json';
}
public function tearDown() {
@@ -63,10 +63,20 @@ class LibraryTest extends \lithium\test\Unit {
$result = $this->library->config('server', 'lab.lithify.me');
$this->assertTrue($result);
+ $expected = array('servers' => array('lab.lithify.me' => true));
+ $result = json_decode(file_get_contents($this->testConf), true);
+ $this->assertEqual($expected, $result);
+
+ //create a new object to test initialiaztion
+ $this->request->params += array('conf' => $this->testConf);
+ $library = new Library(array(
+ 'request' => $this->request, 'classes' => $this->classes
+ ));
+
$expected = array('servers' => array(
'lab.lithify.me' => true
));
- $result = json_decode(file_get_contents($this->_conf), true);
+ $result = $this->library->config();
$this->assertEqual($expected, $result);
}
@@ -74,12 +84,11 @@ class LibraryTest extends \lithium\test\Unit {
$this->skipIf(!extension_loaded('zlib'), 'The zlib extension is not loaded.');
$this->library->library = 'library_test';
- $expected = true;
$result = $this->library->extract($this->_testPath . '/library_test');
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
$expected = "library_test created in {$this->_testPath} from ";
- $expected .= LITHIUM_LIBRARY_PATH
+ $expected .= realpath(LITHIUM_LIBRARY_PATH)
. "/lithium/console/command/create/template/app.phar.gz\n";
$result = $this->library->response->output;
$this->assertEqual($expected, $result);
@@ -94,33 +103,29 @@ class LibraryTest extends \lithium\test\Unit {
$this->library->library = 'library_test';
- $expected = true;
- $result = $this->library->archive(
- $this->_testPath . '/library_test',
- $this->_testPath . '/library_test'
- );
- $this->assertEqual($expected, $result);
+ $testPath = "{$this->_testPath}/library_test";
+ $result = $this->library->archive($testPath, $testPath);
+ $this->assertTrue($result);
$expected = "library_test.phar.gz created in {$this->_testPath} from ";
$expected .= "{$this->_testPath}/library_test\n";
$result = $this->library->response->output;
$this->assertEqual($expected, $result);
- Phar::unlinkArchive($this->_testPath . '/library_test.phar');
+ Phar::unlinkArchive("{$this->_testPath}/library_test.phar");
}
public function testExtractWithFullPaths() {
$this->skipIf(
- !file_exists($this->_testPath . '/library_test.phar.gz'),
+ !file_exists("{$this->_testPath}/library_test.phar.gz"),
'Skipped test {:class}::{:function}() - depends on {:class}::testArchive()'
);
$this->library->library = 'library_test';
- $expected = true;
$result = $this->library->extract(
$this->_testPath . '/library_test.phar.gz', $this->_testPath . '/new'
);
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
$this->assertTrue(file_exists($this->_testPath . '/new'));
@@ -146,22 +151,20 @@ class LibraryTest extends \lithium\test\Unit {
);
chdir('new');
- $app = new Library(array(
- 'request' => new Request(), 'classes' => $this->classes
- ));
+ $app = new Library(array('request' => new Request(), 'classes' => $this->classes));
$app->library = 'does_not_exist';
- $expected = true;
$result = $app->archive();
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
- $expected = "new.phar.gz created in {$this->_testPath} from ";
- $expected .= "{$this->_testPath}/new\n";
+ $path = realpath($this->_testPath);
+ $expected = "new.phar.gz created in {$path} from {$path}/new\n";
$result = $app->response->output;
$this->assertEqual($expected, $result);
Phar::unlinkArchive($this->_testPath . '/new.phar');
Phar::unlinkArchive($this->_testPath . '/new.phar.gz');
+
$this->_cleanUp('tests/new');
rmdir($this->_testPath . '/new');
}
@@ -169,20 +172,17 @@ class LibraryTest extends \lithium\test\Unit {
public function testExtractWhenLibraryDoesNotExist() {
$this->skipIf(!extension_loaded('zlib'), 'The zlib extension is not loaded.');
chdir($this->_testPath);
- $app = new Library(array(
- 'request' => new Request(), 'classes' => $this->classes
- ));
+ $app = new Library(array('request' => new Request(), 'classes' => $this->classes));
$app->library = 'does_not_exist';
- $expected = true;
$result = $app->extract();
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
$this->assertTrue(file_exists($this->_testPath . '/new'));
- $expected = "new created in {$this->_testPath} from ";
- $expected .= LITHIUM_LIBRARY_PATH
- . "/lithium/console/command/create/template/app.phar.gz\n";
+ $path = realpath($this->_testPath);
+ $tplPath = realpath(LITHIUM_LIBRARY_PATH) . '/lithium/console/command/create/template';
+ $expected = "new created in {$path} from {$tplPath}/app.phar.gz\n";
$result = $app->response->output;
$this->assertEqual($expected, $result);
@@ -192,28 +192,97 @@ class LibraryTest extends \lithium\test\Unit {
public function testExtractPlugin() {
$this->skipIf(!extension_loaded('zlib'), 'The zlib extension is not loaded.');
$this->library->library = 'library_plugin_test';
+ $path = $this->_testPath;
- $expected = true;
- $result = $this->library->extract('plugin', $this->_testPath . '/library_test_plugin');
- $this->assertEqual($expected, $result);
+ $result = $this->library->extract('plugin', "{$path}/library_test_plugin");
+ $this->assertTrue($result);
- $expected = "library_test_plugin created in {$this->_testPath} from ";
- $expected .= LITHIUM_LIBRARY_PATH
- . "/lithium/console/command/create/template/plugin.phar.gz\n";
+ $expected = "library_test_plugin created in {$path} from " . realpath(LITHIUM_LIBRARY_PATH);
+ $expected .= "/lithium/console/command/create/template/plugin.phar.gz\n";
$result = $this->library->response->output;
$this->assertEqual($expected, $result);
+
+ $this->_cleanup();
}
public function testFormulate() {
- $result = $this->library->formulate(
- $this->_testPath . '/library_test_plugin'
- );
+ $this->library->formulate();
+ $expected = '/please supply a name/';
+ $result = $this->library->response->output;
+ $this->assertPattern($expected, $result);
+
+ $path = $this->_testPath . '/library_test_plugin';
+ mkdir($path);
+ $result = $this->library->formulate($path);
+ $this->assertTrue($result);
+
+ $result = file_exists($path . '/config/library_test_plugin.json');
$this->assertTrue($result);
- $result = file_exists(
- $this->_testPath
- . '/library_test_plugin/config/library_test_plugin.json'
+ $this->_cleanUp();
+ }
+
+ public function testFormulateWithFormula() {
+ $path = $this->_testPath . '/library_test_plugin';
+ mkdir($path);
+ mkdir($path . '/config');
+ file_put_contents(
+ $path . '/config/library_test_plugin.json',
+ json_encode(array(
+ 'name' => 'library_test_plugin',
+ 'version' => '1.0',
+ 'summary' => 'something',
+ 'sources' => array(
+ 'phar' => 'http://somewhere.com/download/library_test_plugin.phar.gz'
+ )
+ ))
);
+
+ $result = $this->library->formulate($path);
+ $this->assertTrue($result);
+
+ $result = file_exists($path . '/config/library_test_plugin.json');
+ $this->assertTrue($result);
+ }
+
+ public function testNoFormulate() {
+ $path = $this->_testPath . '/library_test_no_plugin';
+ $result = $this->library->formulate($path);
+ $this->assertFalse($result);
+
+ $result = file_exists($path . '/config/library_test_no_plugin.json');
+ $this->assertFalse($result);
+
+ $expected = '/Formula for library_test_no_plugin not created/';
+ $result = $this->library->response->error;
+ $this->assertPattern($expected, $result);
+ }
+
+ public function testFormulateNoPath() {
+ $path = $this->_testPath . '/library_test_no_plugin';
+ umask(0);
+ mkdir($path, 655);
+ umask(100);
+ $this->expectException('/Permission denied/');
+
+ $result = $this->library->formulate($path);
+ $this->assertFalse($result);
+
+ $result = file_exists($path . '/config/library_test_plugin.json');
+ $this->assertFalse($result);
+
+ $expected = '/Formula for library_test_no_plugin not created/';
+ $result = $this->library->response->error;
+ $this->assertPattern($expected, $result);
+
+ umask(0);
+ rmdir($path);
+ }
+
+ public function testPushNoName() {
+ $this->library->push();
+ $expected = 'please supply a name';
+ $result = $this->library->response->output;
$this->assertTrue($result);
}
@@ -288,6 +357,58 @@ class LibraryTest extends \lithium\test\Unit {
$this->_cleanUp();
}
+ public function testNoInstall() {
+ $result = $this->library->install('library_test_plugin');
+ $expected = "library_test_plugin not installed.\n";
+ $result = $this->library->response->output;
+ $this->assertEqual($expected, $result);
+ $this->library->response->output = null;
+
+ $this->request->params += array('server' => null);
+ $library = new Library(array(
+ 'request' => $this->request, 'classes' => $this->classes
+ ));
+ $library->conf = $this->testConf;
+ $library->config('server', 'localhost');
+ $result = $this->library->install('library_not_a_plugin');
+ $expected = "library_not_a_plugin not found.\n";
+ $result = $this->library->response->error;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testNoInstalLab() {
+ $this->skipIf(!extension_loaded('zlib'), 'The zlib extension is not loaded.');
+ $this->skipIf(
+ ini_get('phar.readonly') == '1',
+ 'Skipped test {:class}::{:function}() - relies on {:class}::testPush()'
+ );
+ $this->library->path = $this->_testPath;
+ $result = $this->library->install('li3_lab');
+
+ $expected = "li3_lab not installed.\n";
+ $result = $this->library->response->output;
+ $this->assertEqual($expected, $result);
+
+ $result = is_dir($this->_testPath . '/li3_lab');
+ $this->assertFalse($result);
+ $this->_cleanUp();
+ }
+
+ public function testInstallDocs() {
+ $this->skipIf(strpos(shell_exec('git --version'), 'git version') === false,
+ 'The git is not installed.'
+ );
+ $this->skipIf(dns_check_record("google.com") === false, "No internet connection.");
+
+ $this->library->path = $this->_testPath;
+ $result = $this->library->install('li3_docs');
+ $this->assertTrue($result);
+
+ $result = is_dir($this->_testPath . '/li3_docs');
+ $this->assertTrue($result);
+ $this->_cleanUp();
+ }
+
public function testFind() {
$this->library->find();
@@ -306,7 +427,18 @@ Version: 1.0
Created: 2009-11-30
test;
- $result = $this->library->response->output;
+ }
+
+ public function testFindNotFound() {
+ $this->request->params += array('server' => null);
+ $library = new Library(array(
+ 'request' => $this->request, 'classes' => $this->classes
+ ));
+ $library->conf = $this->testConf;
+ $library->config('server', 'localhost');
+ $library->find();
+ $expected = "No plugins at localhost\n";
+ $result = $library->response->output;
$this->assertEqual($expected, $result);
}
@@ -453,6 +585,73 @@ test;
Phar::unlinkArchive($this->_testPath . '/library_test_plugin.phar.gz');
$this->_cleanUp();
}
+
+
+ public function testPushNotValid() {
+ $this->skipIf(!extension_loaded('zlib'), 'The zlib extension is not loaded.');
+ $this->skipIf(
+ ini_get('phar.readonly') == '1',
+ 'Skipped test {:class}::{:function}() - INI setting phar.readonly = On'
+ );
+ $this->library->library = 'library_plugin_test';
+ $path = $this->_testPath;
+
+ $result = $this->library->extract('plugin', "{$path}/library_test_plugin");
+ $this->assertTrue($result);
+ $this->library->response->output = null;
+
+ $file = $this->_testPath . '/library_test_plugin/config/library_test_plugin.json';
+ $result = file_put_contents(
+ $file,
+ json_encode(array(
+ 'name' => 'library_test_plugin',
+ 'version' => '1.0',
+ 'summary' => 'something',
+ ))
+ );
+ $this->assertTrue($result);
+
+ $result = $this->library->archive(
+ $this->_testPath . '/library_test_plugin',
+ $this->_testPath . '/library_test_plugin'
+ );
+ $this->assertTrue($result);
+
+ $expected = "library_test_plugin.phar.gz created in {$this->_testPath} from ";
+ $expected .= "{$this->_testPath}/library_test_plugin\n";
+ $result = $this->library->response->output;
+ $this->assertEqual($expected, $result);
+
+ $result = file_exists($this->_testPath . '/library_test_plugin.phar.gz');
+ $this->assertTrue($result);
+ $this->library->response->output = null;
+
+ $result = $this->library->push('library_test_plugin');
+ $this->assertFalse($result);
+
+ $expected = "/The forumla for library_test_plugin is not valid/";
+ $result = $this->library->response->error;
+ $this->assertPattern($expected, $result);
+
+ $result = is_dir($this->_testPath . '/library_test_plugin');
+ $this->assertTrue($result);
+
+ Phar::unlinkArchive($this->_testPath . '/library_test_plugin.phar');
+ Phar::unlinkArchive($this->_testPath . '/library_test_plugin.phar.gz');
+ $this->_cleanUp();
+ }
+
+ public function testNoArchive() {
+ $result = $this->library->archive(
+ $this->_testPath . '/library_test_plugin',
+ $this->_testPath . '/library_test_plugin'
+ );
+ $this->assertFalse($result);
+
+ $expected = "/Could not create archive from/";
+ $result = $this->library->response->error;
+ $this->assertPattern($expected, $result);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/command/RouteTest.php b/libraries/lithium/tests/cases/console/command/RouteTest.php
new file mode 100644
index 0000000..f50d5e6
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/command/RouteTest.php
@@ -0,0 +1,289 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\console\command;
+
+use lithium\console\command\Route;
+use lithium\console\Request;
+use lithium\net\http\Router;
+
+/**
+ * The RouteTest class tests the "li3 route" command.
+ */
+class RouteTest extends \lithium\test\Unit {
+
+ /**
+ * Holds config params.
+ *
+ * @var array
+ */
+ protected $_config = array('routes_file' => '');
+
+ /**
+ * Holds the temporary test path.
+ *
+ * @var string
+ */
+ protected $_testPath = null;
+
+ /**
+ * Set the testPath and check if it is writable (skip if not).
+ */
+ public function skip() {
+ $this->_testPath = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not writable.");
+ }
+
+ /**
+ * Create a temporary routes.php file for testing and reset the router.
+ */
+ public function setUp() {
+ $this->_config['routes_file'] = "{$this->_testPath}/routes.php";
+
+ $testParams = 'array("controller" => "lithium\test\Controller")';
+ $content = array(
+ '<?php',
+ 'use lithium\net\http\Router;',
+ 'use lithium\core\Environment;',
+ 'Router::connect("/", "Pages::view");',
+ 'Router::connect("/pages/{:args}", "Pages::view");',
+ 'if (!Environment::is("production")) {',
+ 'Router::connect("/test/{:args}", ' . $testParams . ');',
+ 'Router::connect("/test", ' . $testParams . ');',
+ '}',
+ '?>'
+ );
+ file_put_contents($this->_config['routes_file'], join("\n", $content));
+
+ Router::reset();
+ }
+
+ /**
+ * Delete the temporary routes.php file.
+ */
+ public function tearDown() {
+ if (file_exists($this->_config['routes_file'])) {
+ unlink($this->_config['routes_file']);
+ }
+ }
+
+ /**
+ * Tests if the default environment is loaded correctly
+ * and if overriding works as expected.
+ */
+ public function testEnvironment() {
+ $command = new Route();
+ $expected = 'development';
+ $this->assertEqual($expected, $command->env);
+
+ $request = new Request();
+ $request->params['env'] = 'production';
+ $command = new Route(array('request' => $request));
+ $expected = 'production';
+ $this->assertEqual($expected, $command->env);
+ }
+
+ /**
+ * Test if the routes.php file is loaded correctly and the
+ * routes are connected to the router.
+ */
+ public function testRouteLoading() {
+ $this->assertFalse(Router::get());
+
+ $command = new Route(array('routes_file' => $this->_config['routes_file']));
+ $this->assertEqual(4, count(Router::get()));
+
+ Router::reset();
+
+ $request = new Request();
+ $request->params['env'] = 'production';
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'request' => $request
+ ));
+ $this->assertEqual(2, count(Router::get()));
+ }
+
+ /**
+ * Tests the "all" command without an env param.
+ *
+ * Don't be confused if the expected output doesn't make sense here. We are
+ * stripping the whitespace away so that this source code is easier to read.
+ * Built-In methods are used for output formatting and are tested elsewhere.
+ */
+ public function testAllWithoutEnvironment() {
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => new Request()
+ ));
+
+ $command->all();
+
+ $expected = 'TemplateParams--------------
+ /{"controller":"pages","action":"view"}
+ /pages/{:args}{"controller":"pages","action":"view"}
+ /test/{:args}{"controller":"lithium\\test\\\\Controller","action":"index"}
+ /test{"controller":"lithium\\test\\\\Controller","action":"index"}';
+ $this->assertEqual($this->_strip($expected),$this->_strip($command->response->output));
+ }
+
+ /**
+ * Tests the "all" command with an env (production) param.
+ *
+ * Don't be confused if the expected output doesn't make sense here. We are
+ * stripping the whitespace away so that this source code is easier to read.
+ * Built-In methods are used for output formatting and are tested elsewhere.
+ */
+ public function testAllWithEnvironment() {
+ $request = new Request();
+ $request->params = array(
+ 'env' => 'production'
+ );
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => $request
+ ));
+
+ $command->all();
+
+ $expected = 'TemplateParams--------------
+ /{"controller":"pages","action":"view"}
+ /pages/{:args}{"controller":"pages","action":"view"}';
+ $this->assertEqual($this->_strip($expected),$this->_strip($command->response->output));
+ }
+
+ /**
+ * Test the alias method for "all".
+ */
+ public function testRun() {
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => new Request()
+ ));
+
+ $command->run();
+
+ $expected = 'TemplateParams--------------
+ /{"controller":"pages","action":"view"}
+ /pages/{:args}{"controller":"pages","action":"view"}
+ /test/{:args}{"controller":"lithium\\test\\\\Controller","action":"index"}
+ /test{"controller":"lithium\\test\\\\Controller","action":"index"}';
+ $this->assertEqual($this->_strip($expected),$this->_strip($command->response->output));
+ }
+
+ /**
+ * Test the show command with no route.
+ */
+ public function testShowWithNoRoute() {
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => new Request()
+ ));
+
+ $command->show();
+
+ $expected = "Please provide a valid URL\n";
+ $this->assertEqual($expected, $command->response->error);
+ }
+
+ /**
+ * Test the show command with an invalid route.
+ */
+ public function testShowWithInvalidRoute() {
+ $request = new Request();
+ $request->params = array(
+ 'args' => array('/foobar')
+ );
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => $request
+ ));
+ $command->show();
+
+ $expected = "No route found.\n";
+ $this->assertEqual($expected, $command->response->output);
+ }
+
+ /**
+ * Test the show command with a valid route.
+ */
+ public function testShowWithValidRoute() {
+ $request = new Request();
+ $request->params = array('args' => array('/'));
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => $request
+ ));
+ $command->show();
+
+ $expected = "{\"controller\":\"pages\",\"action\":\"view\"}\n";
+ $this->assertEqual($expected, $command->response->output);
+ }
+
+ /**
+ * Test the show command with a env param.
+ */
+ public function testShowWithEnvironment() {
+ $request = new Request();
+ $request->params = array(
+ 'env' => 'production',
+ 'args' => array('/test')
+ );
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => $request
+ ));
+
+ $command->show();
+
+ $expected = "No route found.\n";
+ $this->assertEqual($expected, $command->response->output);
+ }
+
+ /**
+ * Test the show command with http method.
+ *
+ * This tests a call similar to "li3 route GET /".
+ */
+ public function testShowWithHttpMethod() {
+ $request = new Request();
+ $request->params = array(
+ 'args' => array('post', '/')
+ );
+ $command = new Route(array(
+ 'routes_file' => $this->_config['routes_file'],
+ 'classes' => array('response' => '\lithium\tests\mocks\console\MockResponse'),
+ 'request' => $request
+ ));
+
+ $command->show();
+
+ $expected = "{\"controller\":\"pages\",\"action\":\"view\"}\n";
+ $this->assertEqual($expected, $command->response->output);
+ }
+
+ /**
+ * Remove formatting whitespace, tabs and newlines for better sourcecode
+ * readability.
+ *
+ * @param string $str A string from which to strip spaces
+ * @return string Returns the value of `$str` with all whitespace removed.
+ */
+ protected function _strip($str) {
+ return preg_replace('/\s/', '', $str);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/command/create/ControllerTest.php b/libraries/lithium/tests/cases/console/command/create/ControllerTest.php
index 2c6b483..f4542eb 100644
--- a/libraries/lithium/tests/cases/console/command/create/ControllerTest.php
+++ b/libraries/lithium/tests/cases/console/command/create/ControllerTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console\command\create;
-use \lithium\console\command\create\Controller;
-use \lithium\console\Request;
-use \lithium\core\Libraries;
+use lithium\console\command\create\Controller;
+use lithium\console\Request;
+use lithium\core\Libraries;
class ControllerTest extends \lithium\test\Unit {
@@ -21,12 +21,12 @@ class ControllerTest extends \lithium\test\Unit {
protected $_testPath = null;
public function skip() {
- $this->_testPath = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not writable.");
}
public function setUp() {
- $this->classes = array('response' => '\lithium\tests\mocks\console\MockResponse');
+ $this->classes = array('response' => 'lithium\tests\mocks\console\MockResponse');
$this->_backup['cwd'] = getcwd();
$this->_backup['_SERVER'] = $_SERVER;
$_SERVER['argv'] = array();
@@ -42,12 +42,42 @@ class ControllerTest extends \lithium\test\Unit {
$this->_cleanUp();
}
+ public function testClass() {
+ $this->request->params += array(
+ 'command' => 'controller', 'action' => 'Posts'
+ );
+ $model = new Controller(array(
+ 'request' => $this->request, 'classes' => $this->classes
+ ));
+
+ $expected = 'PostsController';
+ $result = $model->invokeMethod('_class', array($this->request));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testUse() {
+ $this->request->params += array(
+ 'command' => 'controller', 'action' => 'Posts'
+ );
+ $model = new Controller(array(
+ 'request' => $this->request, 'classes' => $this->classes
+ ));
+
+ $expected = 'create_test\\models\\Post';
+ $result = $model->invokeMethod('_use', array($this->request));
+ $this->assertEqual($expected, $result);
+ }
+
public function testRun() {
+ $this->request->params += array(
+ 'command' => 'create', 'action' => 'controller',
+ 'args' => array('Posts')
+ );
$controller = new Controller(array(
'request' => $this->request, 'classes' => $this->classes
));
$controller->path = $this->_testPath;
- $controller->run('Posts');
+ $controller->run('controller');
$expected = "PostsController created in create_test\\controllers.\n";
$result = $controller->response->output;
$this->assertEqual($expected, $result);
@@ -57,7 +87,7 @@ class ControllerTest extends \lithium\test\Unit {
namespace create_test\controllers;
-use \create_test\models\Post;
+use create_test\models\Post;
class PostsController extends \lithium\action\Controller {
@@ -66,39 +96,28 @@ class PostsController extends \lithium\action\Controller {
return compact('posts');
}
- public function view($id = null) {
- $post = Post::find($id);
+ public function view() {
+ $post = Post::first($this->request->id);
return compact('post');
}
public function add() {
- if (!empty($this->request->data)) {
- $post = Post::create($this->request->data);
- if ($post->save()) {
- $this->redirect(array(
- 'controller' => 'posts', 'action' => 'view',
- 'args' => array($post->id)
- ));
- }
- }
- if (empty($post)) {
- $post = Post::create();
+ $post = Post::create();
+
+ if (($this->request->data) && $post->save($this->request->data)) {
+ $this->redirect(array('Posts::view', 'args' => array($post->id)));
}
return compact('post');
}
- public function edit($id = null) {
- $post = Post::find($id);
- if (empty($post)) {
- $this->redirect(array('controller' => 'posts', 'action' => 'index'));
+ public function edit() {
+ $post = Post::find($this->request->id);
+
+ if (!$post) {
+ $this->redirect('Posts::index');
}
- if (!empty($this->request->data)) {
- if ($post->save($this->request->data)) {
- $this->redirect(array(
- 'controller' => 'posts', 'action' => 'view',
- 'args' => array($post->id)
- ));
- }
+ if (($this->request->data) && $post->save($this->request->data)) {
+ $this->redirect(array('Posts::view', 'args' => array($post->id)));
}
return compact('post');
}
diff --git a/libraries/lithium/tests/cases/console/command/create/MockTest.php b/libraries/lithium/tests/cases/console/command/create/MockTest.php
new file mode 100644
index 0000000..e9ee9d2
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/command/create/MockTest.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\console\command\create;
+
+use lithium\console\command\Create;
+use lithium\console\command\create\Mock;
+use lithium\console\Request;
+use lithium\core\Libraries;
+
+class MockTest extends \lithium\test\Unit {
+
+ public $request;
+
+ protected $_backup = array();
+
+ protected $_testPath = null;
+
+ public function skip() {
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
+ $this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not writable.");
+ }
+
+ public function setUp() {
+ $this->classes = array('response' => 'lithium\tests\mocks\console\MockResponse');
+ $this->_backup['cwd'] = getcwd();
+ $this->_backup['_SERVER'] = $_SERVER;
+ $_SERVER['argv'] = array();
+
+ Libraries::add('create_test', array('path' => $this->_testPath . '/create_test'));
+ $this->request = new Request(array('input' => fopen('php://temp', 'w+')));
+ $this->request->params = array('library' => 'create_test');
+ }
+
+ public function tearDown() {
+ $_SERVER = $this->_backup['_SERVER'];
+ chdir($this->_backup['cwd']);
+ $this->_cleanUp();
+ }
+
+ public function testMockModel() {
+ $this->request->params += array(
+ 'command' => 'create', 'action' => 'mock',
+ 'args' => array('model', 'Post')
+ );
+ $mock = new Mock(array(
+ 'request' => $this->request, 'classes' => $this->classes
+ ));
+ $mock->path = $this->_testPath;
+ $mock->run('mock');
+ $expected = "MockPost created in create_test\\tests\\mocks\\models.\n";
+ $result = $mock->response->output;
+ $this->assertEqual($expected, $result);
+
+ $expected = <<<'test'
+
+
+namespace create_test\tests\mocks\models;
+
+class MockPost extends \create_test\models\Post {
+
+
+}
+
+
+test;
+ $replace = array("<?php", "?>");
+ $result = str_replace($replace, '',
+ file_get_contents($this->_testPath . '/create_test/tests/mocks/models/MockPost.php')
+ );
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/command/create/ModelTest.php b/libraries/lithium/tests/cases/console/command/create/ModelTest.php
index 4a205b0..1165968 100644
--- a/libraries/lithium/tests/cases/console/command/create/ModelTest.php
+++ b/libraries/lithium/tests/cases/console/command/create/ModelTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console\command\create;
-use \lithium\console\command\create\Model;
-use \lithium\console\Request;
-use \lithium\core\Libraries;
+use lithium\console\command\create\Model;
+use lithium\console\Request;
+use lithium\core\Libraries;
class ModelTest extends \lithium\test\Unit {
@@ -21,12 +21,12 @@ class ModelTest extends \lithium\test\Unit {
protected $_testPath = null;
public function skip() {
- $this->_testPath = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not readable.");
}
public function setUp() {
- $this->classes = array('response' => '\lithium\tests\mocks\console\MockResponse');
+ $this->classes = array('response' => 'lithium\tests\mocks\console\MockResponse');
$this->_backup['cwd'] = getcwd();
$this->_backup['_SERVER'] = $_SERVER;
$_SERVER['argv'] = array();
@@ -42,32 +42,16 @@ class ModelTest extends \lithium\test\Unit {
$this->_cleanUp();
}
- public function testRun() {
+ public function testClass() {
+ $this->request->params = array(
+ 'command' => 'model', 'action' => 'Post'
+ );
$model = new Model(array(
'request' => $this->request, 'classes' => $this->classes
));
- $model->path = $this->_testPath;
- $model->run('Post');
- $expected = "Post created in create_test\\models.\n";
- $result = $model->response->output;
- $this->assertEqual($expected, $result);
-
- $expected = <<<'test'
-
-
-namespace create_test\models;
-class Post extends \lithium\data\Model {
-
- public $validates = array();
-}
-
-
-test;
- $replace = array("<?php", "?>");
- $result = str_replace($replace, '',
- file_get_contents($this->_testPath . '/create_test/models/Post.php')
- );
+ $expected = 'Post';
+ $result = $model->invokeMethod('_class', array($this->request));
$this->assertEqual($expected, $result);
}
}
diff --git a/libraries/lithium/tests/cases/console/command/create/TestTest.php b/libraries/lithium/tests/cases/console/command/create/TestTest.php
index e4b7529..b524219 100644
--- a/libraries/lithium/tests/cases/console/command/create/TestTest.php
+++ b/libraries/lithium/tests/cases/console/command/create/TestTest.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\console\command\create;
-use \lithium\console\command\Create;
-use \lithium\console\command\create\Test;
-use \lithium\console\Request;
-use \lithium\core\Libraries;
+use lithium\console\command\Create;
+use lithium\console\command\create\Test;
+use lithium\console\Request;
+use lithium\core\Libraries;
class TestTest extends \lithium\test\Unit {
@@ -22,12 +22,13 @@ class TestTest extends \lithium\test\Unit {
protected $_testPath = null;
public function skip() {
- $this->_testPath = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not writable.");
}
public function setUp() {
- $this->classes = array('response' => '\lithium\tests\mocks\console\MockResponse');
+ Libraries::cache(false);
+ $this->classes = array('response' => 'lithium\tests\mocks\console\MockResponse');
$this->_backup['cwd'] = getcwd();
$this->_backup['_SERVER'] = $_SERVER;
$_SERVER['argv'] = array();
@@ -43,13 +44,17 @@ class TestTest extends \lithium\test\Unit {
$this->_cleanUp();
}
- public function testModel() {
+ public function testTestModel() {
+ $this->request->params += array(
+ 'command' => 'create', 'action' => 'test',
+ 'args' => array('model', 'Post')
+ );
$test = new Test(array(
'request' => $this->request, 'classes' => $this->classes
));
$test->path = $this->_testPath;
- $test->run('model', 'Post');
- $expected = "PostTest created for Post in create_test\\tests\\cases\\models.\n";
+ $test->run('test');
+ $expected = "PostTest created in create_test\\tests\\cases\\models.\n";
$result = $test->response->output;
$this->assertEqual($expected, $result);
@@ -58,7 +63,7 @@ class TestTest extends \lithium\test\Unit {
namespace create_test\tests\cases\models;
-use \create_test\models\Post;
+use create_test\models\Post;
class PostTest extends \lithium\test\Unit {
@@ -78,32 +83,51 @@ test;
$this->assertEqual($expected, $result);
}
- public function testMockModel() {
- $test = new Test(array(
- 'request' => $this->request, 'classes' => $this->classes
+ public function testTestModelWithMethods() {
+ $this->_cleanUp();
+ mkdir($this->_testPath . '/create_test/models/', 0755, true);
+ $id = rand();
+ $path = "create_test/models/Post{$id}.php";
+ file_put_contents("{$this->_testPath}/{$path}",
+"<?php
+namespace create_test\models;
+
+class Post{$id} {
+ public function someMethod() {}
+}"
+);
+
+ $this->request->params += array('command' => 'create', 'action' => 'test', 'args' => array(
+ 'model', "Post{$id}"
));
+ $test = new Test(array('request' => $this->request, 'classes' => $this->classes));
$test->path = $this->_testPath;
- $test->mock('model', 'Post');
- $expected = "MockPost created for Post in create_test\\tests\\mocks\\models.\n";
+ $test->run('test');
+ $expected = "Post{$id}Test created in create_test\\tests\\cases\\models.\n";
$result = $test->response->output;
$this->assertEqual($expected, $result);
- $expected = <<<'test'
+ $expected = <<<test
-namespace create_test\tests\mocks\models;
+namespace create_test\\tests\\cases\\models;
-class MockPost extends \create_test\models\Post {
+use create_test\\models\\Post{$id};
+class Post{$id}Test extends \\lithium\\test\\Unit {
+ public function setUp() {}
+
+ public function tearDown() {}
+
+ public function testSomeMethod() {}
}
test;
$replace = array("<?php", "?>");
- $result = str_replace($replace, '',
- file_get_contents($this->_testPath . '/create_test/tests/mocks/models/MockPost.php')
- );
+ $path = "create_test/tests/cases/models/Post{$id}Test.php";
+ $result = str_replace($replace, '', file_get_contents("{$this->_testPath}/{$path}"));
$this->assertEqual($expected, $result);
}
}
diff --git a/libraries/lithium/tests/cases/console/command/create/ViewTest.php b/libraries/lithium/tests/cases/console/command/create/ViewTest.php
new file mode 100644
index 0000000..af60183
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/command/create/ViewTest.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\console\command\create;
+
+use lithium\console\command\Create;
+use lithium\console\command\create\View;
+use lithium\console\Request;
+use lithium\core\Libraries;
+
+class ViewTest extends \lithium\test\Unit {
+
+ public $request;
+
+ protected $_backup = array();
+
+ protected $_testPath = null;
+
+ public function skip() {
+ $this->_testPath = Libraries::get(true, 'resources') . '/tmp/tests';
+ $this->skipIf(!is_writable($this->_testPath), "{$this->_testPath} is not writable.");
+ }
+
+ public function setUp() {
+ $this->classes = array('response' => 'lithium\tests\mocks\console\MockResponse');
+ $this->_backup['cwd'] = getcwd();
+ $this->_backup['_SERVER'] = $_SERVER;
+ $_SERVER['argv'] = array();
+
+ Libraries::add('create_test', array('path' => $this->_testPath . '/create_test'));
+ $this->request = new Request(array('input' => fopen('php://temp', 'w+')));
+ $this->request->params = array('library' => 'create_test');
+ }
+
+ public function tearDown() {
+ $_SERVER = $this->_backup['_SERVER'];
+ chdir($this->_backup['cwd']);
+ $this->_cleanUp();
+ }
+
+ public function testIndexView() {
+ $this->request->params += array(
+ 'command' => 'create', 'action' => 'view',
+ 'args' => array('Posts', 'index.html')
+ );
+ $view = new View(array(
+ 'request' => $this->request, 'classes' => $this->classes
+ ));
+
+ $view->run('view');
+ $expected = "index.html.php created in views/posts.\n";
+ $result = $view->response->output;
+ $this->assertEqual($expected, $result);
+
+ $result = file_exists($this->_testPath . '/create_test/views/posts/index.html.php');
+ $this->assertTrue($result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/command/g11n/ExtractTest.php b/libraries/lithium/tests/cases/console/command/g11n/ExtractTest.php
new file mode 100644
index 0000000..de0eb41
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/command/g11n/ExtractTest.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\console\command\g11n;
+
+use lithium\core\Libraries;
+use lithium\console\Request;
+use lithium\console\command\g11n\Extract;
+
+class ExtractTest extends \lithium\test\Unit {
+
+ protected $_path;
+
+ public $command;
+
+ public function skip() {
+ $this->_path = Libraries::get(true, 'resources') . '/tmp/tests';
+ $this->skipIf(!is_writable($this->_path), "{$this->_path} is not writable.");
+ }
+
+ public function setUp() {
+ $this->command = new Extract(array(
+ 'request' => new Request(array('input' => fopen('php://temp', 'w+'))),
+ 'classes' => array('response' => 'lithium\tests\mocks\console\MockResponse')
+ ));
+ mkdir($this->command->source = "{$this->_path}/source");
+ mkdir($this->command->destination = "{$this->_path}/destination");
+ }
+
+ public function tearDown() {
+ $this->_cleanUp();
+ }
+
+ public function testInit() {
+ $command = new Extract();
+ $this->assertEqual(LITHIUM_APP_PATH, $command->source);
+ $this->assertEqual(Libraries::get(true, 'resources') . '/g11n', $command->destination);
+ }
+
+ public function testFailRead() {
+ $result = $this->command->run();
+ $expected = 1;
+ $this->assertIdentical($expected, $result);
+
+ $expected = "Yielded no items.\n";
+ $result = $this->command->response->error;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testFailWrite() {
+ rmdir($this->command->destination);
+
+ $file = "{$this->_path}/source/a.html.php";
+ $data = <<<EOD
+<h2>Flowers</h2>
+<?=\$t('Apples are green.'); ?>
+EOD;
+ file_put_contents($file, $data);
+
+ $result = $this->command->run();
+ $expected = 1;
+ $this->assertIdentical($expected, $result);
+
+ $expected = "Failed to write template.\n";
+ $result = $this->command->response->error;
+ $this->assertEqual($expected, $result);
+ }
+
+
+ public function testDefaultConfiguration() {
+ $file = "{$this->_path}/source/a.html.php";
+ $data = <<<EOD
+<h2>Flowers</h2>
+<?=\$t('Apples are green.'); ?>
+EOD;
+ file_put_contents($file, $data);
+
+ $result = $this->command->run();
+ $expected = 0;
+ $this->assertIdentical($expected, $result);
+
+ $expected = '/.*Yielded 1 item.*/';
+ $result = $this->command->response->output;
+ $this->assertPattern($expected, $result);
+
+ $file = "{$this->_path}/destination/message_default.pot";
+ $result = file_exists($file);
+ $this->assertTrue($result);
+
+ $result = file_get_contents($file);
+ $expected = '/msgid "Apples are green\."/';
+ $this->assertPattern($expected, $result);
+
+ $expected = '#/resources/tmp/tests/source/a.html.php:2#';
+ $this->assertPattern($expected, $result);
+
+ $result = $this->command->response->error;
+ $this->assertFalse($result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/AdaptableTest.php b/libraries/lithium/tests/cases/core/AdaptableTest.php
index 87bea9f..ac0f827 100644
--- a/libraries/lithium/tests/cases/core/AdaptableTest.php
+++ b/libraries/lithium/tests/cases/core/AdaptableTest.php
@@ -2,19 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\core;
-use \lithium\util\Collection;
-use \lithium\core\Adaptable;
-use \lithium\storage\cache\adapter\Memory;
-use \lithium\storage\cache\strategy\Serializer;
-use \lithium\tests\mocks\core\MockAdapter;
-use \lithium\tests\mocks\core\MockStrategy;
-use \SplDoublyLinkedList;
+use SplDoublyLinkedList;
+use lithium\util\Collection;
+use lithium\core\Adaptable;
+use lithium\storage\cache\adapter\Memory;
+use lithium\tests\mocks\core\MockAdapter;
+use lithium\tests\mocks\core\MockStrategy;
+use lithium\tests\mocks\storage\cache\strategy\MockSerializer;
+use lithium\tests\mocks\storage\cache\strategy\MockConfigurizer;
class AdaptableTest extends \lithium\test\Unit {
@@ -26,7 +27,7 @@ class AdaptableTest extends \lithium\test\Unit {
$this->assertFalse($this->adaptable->config());
$items = array(array(
- 'adapter' => '\some\adapter',
+ 'adapter' => 'some\adapter',
'filters' => array('filter1', 'filter2')
));
$result = $this->adaptable->config($items);
@@ -37,7 +38,7 @@ class AdaptableTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$items = array(array(
- 'adapter' => '\some\adapter',
+ 'adapter' => 'some\adapter',
'filters' => array('filter1', 'filter2')
));
$this->adaptable->config($items);
@@ -63,17 +64,13 @@ class AdaptableTest extends \lithium\test\Unit {
public function testNonExistentConfig() {
$adapter = new MockAdapter();
- $this->expectException('Configuration non_existent_config has not been defined');
- $result = $adapter::adapter('non_existent_config');
- $this->assertNull($result);
+ $this->expectException("Configuration `non_existent_config` has not been defined.");
+ $adapter::adapter('non_existent_config');
}
public function testAdapter() {
$adapter = new MockAdapter();
- $items = array('default' => array(
- 'adapter' => 'Memory',
- 'filters' => array()
- ));
+ $items = array('default' => array('adapter' => 'Memory', 'filters' => array()));
$adapter::config($items);
$result = $adapter::config();
$expected = $items;
@@ -84,10 +81,29 @@ class AdaptableTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testConfigAndAdapter() {
+ $adapter = new MockAdapter();
+ $items = array('default' => array('adapter' => 'Memory', 'filters' => array()));
+ $adapter::config($items);
+ $config = $adapter::config();
+
+ $intermediate = $adapter::adapter('default');
+ $expected = new Memory($items['default']);
+ $this->assertEqual($expected, $intermediate);
+
+ $result = $adapter::config();
+ $modified['default'] = $config['default'] + array('object' => $intermediate);
+ $this->assertEqual($modified, $result);
+
+ $adapter::config(array('default' => array('adapter' => 'Memory')));
+ $result = $adapter::config();
+ $this->assertEqual($config, $result);
+ }
+
public function testStrategy() {
$strategy = new MockStrategy();
$items = array('default' => array(
- 'strategies' => array('Serializer'),
+ 'strategies' => array('lithium\tests\mocks\storage\cache\strategy\MockSerializer'),
'filters' => array(),
'adapter' => null
));
@@ -99,19 +115,58 @@ class AdaptableTest extends \lithium\test\Unit {
$result = $strategy::strategies('default');
$this->assertTrue($result instanceof SplDoublyLinkedList);
$this->assertEqual(count($result), 1);
- $this->assertTrue($result->top() instanceof Serializer);
+ $this->assertTrue($result->top() instanceof MockSerializer);
+ }
+
+ public function testInvalidStrategy() {
+ $strategy = new MockStrategy();
+ $items = array('default' => array(
+ 'strategies' => array('InvalidStrategy'),
+ 'filters' => array(),
+ 'adapter' => null
+ ));
+ $strategy::config($items);
+
+ $class = 'lithium\tests\mocks\core\MockStrategy';
+ $message = "Could not find strategy `InvalidStrategy` in class `{$class}`.";
+ $this->expectException($message);
+
+ $result = $strategy::strategies('default');
+ $this->assertTrue($result instanceof SplDoublyLinkedList);
+ }
+
+ public function testStrategyConstructionSettings() {
+ $strategy = new MockStrategy();
+ $items = array('default' => array(
+ 'strategies' => array(
+ 'lithium\tests\mocks\storage\cache\strategy\MockConfigurizer' => array(
+ 'key1' => 'value1', 'key2' => 'value2'
+ )
+ ),
+ 'filters' => array(),
+ 'adapter' => null
+ ));
+ $strategy::config($items);
+ $result = $strategy::config();
+ $expected = $items;
+ $this->assertEqual($expected, $result);
+
+ $result = $strategy::strategies('default');
+ $this->assertTrue($result instanceof SplDoublyLinkedList);
+ $this->assertEqual(count($result), 1);
+ $this->assertTrue($result->top() instanceof MockConfigurizer);
}
public function testNonExistentStrategyConfiguration() {
$strategy = new MockStrategy();
- $this->expectException('Configuration non_existent_config has not been defined');
+ $this->expectException("Configuration `non_existent_config` has not been defined.");
$result = $strategy::strategies('non_existent_config');
$this->assertNull($result);
}
public function testApplyStrategiesNonExistentConfiguration() {
$strategy = new MockStrategy();
- $this->expectException('Configuration non_existent_config has not been defined');
+ $this->expectException("Configuration `non_existent_config` has not been defined.");
$strategy::applyStrategies('method', 'non_existent_config', null);
}
@@ -120,7 +175,7 @@ class AdaptableTest extends \lithium\test\Unit {
$items = array('default' => array(
'filters' => array(),
'adapter' => null,
- 'strategies' => array('Serializer')
+ 'strategies' => array('lithium\tests\mocks\storage\cache\strategy\MockSerializer'),
));
$strategy::config($items);
$result = $strategy::config();
@@ -132,12 +187,33 @@ class AdaptableTest extends \lithium\test\Unit {
$this->assertEqual(serialize($data), $result);
}
+ public function testApplySingleStrategyWithConfiguration() {
+ $strategy = new MockStrategy();
+ $params = array('key1' => 'value1', 'key2' => 'value2');
+ $items = array('default' => array(
+ 'filters' => array(),
+ 'adapter' => null,
+ 'strategies' => array(
+ 'lithium\tests\mocks\storage\cache\strategy\MockConfigurizer' => $params
+ )
+ ));
+ $strategy::config($items);
+ $result = $strategy::config();
+ $expected = $items;
+ $this->assertEqual($expected, $result);
+
+ $result = $strategy::applyStrategies('write', 'default', null);
+ $this->assertEqual($params, $result);
+ }
+
public function testApplyMultipleStrategies() {
$strategy = new MockStrategy();
$items = array('default' => array(
'filters' => array(),
'adapter' => null,
- 'strategies' => array('Serializer', 'Base64')
+ 'strategies' => array(
+ 'lithium\tests\mocks\storage\cache\strategy\MockSerializer', 'Base64'
+ )
));
$strategy::config($items);
$result = $strategy::config();
@@ -149,7 +225,8 @@ class AdaptableTest extends \lithium\test\Unit {
$transformed = base64_encode(serialize($data));
$this->assertEqual($transformed, $result);
- $result = $strategy::applyStrategies('read', 'default', $transformed, 'LIFO');
+ $options = array('mode' => 'LIFO');
+ $result = $strategy::applyStrategies('read', 'default', $transformed, $options);
$expected = $data;
$this->assertEqual($expected, $result);
}
@@ -187,10 +264,7 @@ class AdaptableTest extends \lithium\test\Unit {
public function testEnabled() {
$adapter = new MockAdapter();
- $items = array('default' => array(
- 'adapter' => 'Memory',
- 'filters' => array()
- ));
+ $items = array('default' => array('adapter' => 'Memory', 'filters' => array()));
$adapter::config($items);
$result = $adapter::config();
$expected = $items;
@@ -207,16 +281,14 @@ class AdaptableTest extends \lithium\test\Unit {
public function testNonExistentAdapter() {
$adapter = new MockAdapter();
- $items = array('default' => array(
- 'adapter' => 'NonExistent', 'filters' => array()
- ));
+ $items = array('default' => array('adapter' => 'NonExistent', 'filters' => array()));
$adapter::config($items);
$result = $adapter::config();
$expected = $items;
$this->assertEqual($expected, $result);
- $message = 'Could not find adapter NonExistent in ';
- $message .= 'class lithium\tests\mocks\core\MockAdapter';
+ $message = 'Could not find adapter `NonExistent` in ';
+ $message .= 'class `lithium\tests\mocks\core\MockAdapter`.';
$this->expectException($message);
$result = $adapter::adapter('default');
@@ -249,7 +321,7 @@ class AdaptableTest extends \lithium\test\Unit {
$adapter::config($items);
$message = 'No adapter set for configuration in ';
- $message .= 'class lithium\tests\mocks\core\MockAdapter';
+ $message .= 'class `lithium\tests\mocks\core\MockAdapter`.';
$this->expectException($message);
$result = $adapter::adapter('default');
}
diff --git a/libraries/lithium/tests/cases/core/EnvironmentTest.php b/libraries/lithium/tests/cases/core/EnvironmentTest.php
index c9cf175..064f05c 100644
--- a/libraries/lithium/tests/cases/core/EnvironmentTest.php
+++ b/libraries/lithium/tests/cases/core/EnvironmentTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\core;
-use \lithium\core\Environment;
-use \lithium\tests\mocks\core\MockRequest;
+use lithium\core\Environment;
+use lithium\tests\mocks\core\MockRequest;
class EnvironmentTest extends \lithium\test\Unit {
@@ -144,6 +144,23 @@ class EnvironmentTest extends \lithium\test\Unit {
Environment::set($request);
$this->assertTrue(Environment::is('production'));
}
+
+ /**
+ * Tests calling `get()` and `set()` with `true` as the envrionment name, to automatically
+ * select the current environment.
+ *
+ * @return void
+ */
+ public function testReadWriteWithDefaultEnvironment() {
+ Environment::set('development');
+ Environment::set(true, array('foo' => 'bar'));
+
+ $this->assertEqual(array('foo' => 'bar'), Environment::get('development'));
+ $this->assertEqual(Environment::get(true), Environment::get('development'));
+
+ Environment::set('production');
+ $this->assertFalse(Environment::get(true));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/ErrorHandlerTest.php b/libraries/lithium/tests/cases/core/ErrorHandlerTest.php
new file mode 100644
index 0000000..bd37732
--- /dev/null
+++ b/libraries/lithium/tests/cases/core/ErrorHandlerTest.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2009, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\core;
+
+use Closure;
+use Exception;
+use UnexpectedValueException;
+use lithium\core\ErrorHandler;
+
+class ErrorHandlerTest extends \lithium\test\Unit {
+
+ public $errors = array();
+
+ public function setUp() {
+ if (!ErrorHandler::isRunning()) {
+ ErrorHandler::run();
+ }
+ ErrorHandler::reset();
+ $this->errors = array();
+ }
+
+ public function tearDown() {
+ if (ErrorHandler::isRunning()) {
+ ErrorHandler::stop();
+ }
+ }
+
+ public function testExceptionCatching() {
+ $self = $this;
+ ErrorHandler::config(array(array(
+ 'type' => 'Exception',
+ 'handler' => function($info) use ($self) {
+ $self->errors[] = $info;
+ }
+ )));
+
+ ErrorHandler::handle(new Exception('Test!'));
+
+ $this->assertEqual(1, count($this->errors));
+ $result = end($this->errors);
+ $expected = 'Test!';
+ $this->assertEqual($expected, $result['message']);
+
+ $this->expectException('/Test/');
+ trigger_error('Test warning!', E_USER_WARNING);
+ $this->assertEqual(1, count($this->errors));
+ }
+
+ public function testExceptionSubclassCatching() {
+ $self = $this;
+ ErrorHandler::config(array(array(
+ 'type' => 'Exception',
+ 'handler' => function($info) use ($self) {
+ $self->errors[] = $info;
+ }
+ )));
+ ErrorHandler::handle(new UnexpectedValueException('Test subclass'));
+
+ $this->assertEqual(1, count($this->errors));
+ $result = end($this->errors);
+ $expected = 'Test subclass';
+ $this->assertEqual($expected, $result['message']);
+ }
+
+ public function testErrorCatching() {
+ $this->skipIf(true, 'Refactoring original error-handling iteration.');
+
+ $self = $this;
+ ErrorHandler::config(array(array(
+ 'code' => E_WARNING | E_USER_WARNING,
+ 'handler' => function($info) use ($self) {
+ $self->errors[] = $info;
+ }
+ )));
+
+ file_get_contents(false);
+ $this->assertEqual(1, count($this->errors));
+
+ $result = end($this->errors);
+ $this->assertPattern('/Filename cannot be empty/', $result['message']);
+
+ trigger_error('Test warning', E_USER_WARNING);
+ $this->assertEqual(2, count($this->errors));
+
+ $result = end($this->errors);
+ $this->assertEqual('Test warning', $result['message']);
+
+ trigger_error('Test notice', E_USER_NOTICE);
+ $this->assertEqual(2, count($this->errors));
+ }
+
+ public function testReset() {
+ ErrorHandler::reset();
+ $this->assertEqual(array(), ErrorHandler::handlers());
+
+ $result = ErrorHandler::handlers(array('test' => function($error) { /* Do something */ }));
+ $this->assertEqual(array('test'), array_keys($result));
+ $this->assertTrue($result['test'] instanceof Closure);
+ $this->assertEqual($result, ErrorHandler::handlers());
+
+ ErrorHandler::reset();
+ $this->assertEqual(array(), ErrorHandler::handlers());
+ }
+
+ public function testApply() {
+ $subject = new ErrorHandlerTest();
+ ErrorHandler::apply(array($subject, 'throwException'), array(), function($details) {
+ return $details['exception']->getMessage();
+ });
+ $this->assertEqual('foo', $subject->throwException());
+ }
+
+ public function throwException() {
+ return $this->_filter(__METHOD__, array(), function($self, $params) {
+ throw new Exception('foo');
+ return 'bar';
+ });
+ }
+
+ public function testTrace() {
+ $current = debug_backtrace();
+ $results = ErrorHandler::trace($current);
+ $this->assertEqual(count($current), count($results));
+ $this->assertEqual($results[0], 'lithium\tests\cases\core\ErrorHandlerTest::testTrace');
+ }
+
+ public function testRun() {
+ ErrorHandler::stop();
+ $this->assertEqual(ErrorHandler::isRunning(), false);
+ ErrorHandler::run();
+ $this->assertEqual(ErrorHandler::isRunning(), true);
+ ErrorHandler::stop();
+ $this->assertEqual(ErrorHandler::isRunning(), false);
+ }
+
+ public function testErrorTrapping() {
+ ErrorHandler::stop();
+ ErrorHandler::run(array('trapErrors' => true));
+
+ // Undefined offset error shouldn't surface.
+ list($foo, $bar) = array('baz');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/LibrariesTest.php b/libraries/lithium/tests/cases/core/LibrariesTest.php
index b8aa607..26c0d4e 100644
--- a/libraries/lithium/tests/cases/core/LibrariesTest.php
+++ b/libraries/lithium/tests/cases/core/LibrariesTest.php
@@ -8,34 +8,55 @@
namespace lithium\tests\cases\core;
-use \SplFileInfo;
-use \lithium\core\Libraries;
+use stdClass;
+use SplFileInfo;
+use lithium\util\Inflector;
+use lithium\core\Libraries;
class LibrariesTest extends \lithium\test\Unit {
+ protected $_cache = array();
+
+ public function setUp() {
+ $this->_cache = Libraries::cache();
+ Libraries::cache(false);
+ }
+
+ public function tearDown() {
+ Libraries::cache(false);
+ Libraries::cache($this->_cache);
+ }
+
public function testNamespaceToFileTranslation() {
$result = Libraries::path('\lithium\core\Libraries');
$this->assertTrue(strpos($result, '/lithium/core/Libraries.php'));
$this->assertTrue(file_exists($result));
$this->assertFalse(strpos($result, '\\'));
+
+ $result = Libraries::path('lithium\core\Libraries');
+ $this->assertTrue(strpos($result, '/lithium/core/Libraries.php'));
+ $this->assertTrue(file_exists($result));
+ $this->assertFalse(strpos($result, '\\'));
}
public function testPathTemplate() {
- $expected = array('{:app}/libraries/{:name}', '{:root}/libraries/{:name}');
+ $expected = array('{:app}/libraries/{:name}', '{:root}/{:name}');
$result = Libraries::paths('libraries');
$this->assertEqual($expected, $result);
$this->assertNull(Libraries::locate('authAdapter', 'Form'));
$paths = Libraries::paths();
- $test = array('authAdapter' => array('lithium\security\auth\adapter\{:name}'));
+ $test = array('authAdapter' => array('lithium\security\auth\adapter\{:name}'));
Libraries::paths($test);
-
$this->assertEqual($paths + $test, Libraries::paths());
$class = Libraries::locate('authAdapter', 'Form');
$expected = 'lithium\security\auth\adapter\Form';
$this->assertEqual($expected, $class);
+
+ Libraries::paths($paths + array('authAdapter' => false));
+ $this->assertEqual($paths, Libraries::paths());
}
public function testPathTransform() {
@@ -78,16 +99,17 @@ class LibrariesTest extends \lithium\test\Unit {
public function testLibraryConfigAccess() {
$result = Libraries::get('lithium');
$expected = array(
- 'path' => str_replace('\\', '/', LITHIUM_LIBRARY_PATH) . '/lithium',
- 'loader' => 'lithium\\core\\Libraries::load',
+ 'path' => str_replace('\\', '/', realpath(LITHIUM_LIBRARY_PATH)) . '/lithium',
'prefix' => 'lithium\\',
'suffix' => '.php',
+ 'loader' => 'lithium\\core\\Libraries::load',
+ 'includePath' => false,
'transform' => null,
- 'bootstrap' => null,
+ 'bootstrap' => false,
'defer' => true,
- 'includePath' => false,
'default' => false
);
+
$this->assertEqual($expected, $result);
$this->assertNull(Libraries::get('foo'));
@@ -106,7 +128,7 @@ class LibrariesTest extends \lithium\test\Unit {
$lithium = Libraries::get('lithium');
$this->assertFalse(empty($lithium));
- $app = Libraries::get('app');
+ $app = Libraries::get(true);
$this->assertFalse(empty($app));
Libraries::remove(array('lithium', 'app'));
@@ -117,11 +139,11 @@ class LibrariesTest extends \lithium\test\Unit {
$result = Libraries::get('app');
$this->assertTrue(empty($result));
- $result = Libraries::add('lithium', array('bootstrap' => null) + $lithium);
+ $result = Libraries::add('lithium', array('bootstrap' => false) + $lithium);
$this->assertEqual($lithium, $result);
- $result = Libraries::add('app', array('bootstrap' => null) + $app);
- $this->assertEqual(array('bootstrap' => null) + $app, $result);
+ $result = Libraries::add('app', array('bootstrap' => false) + $app);
+ $this->assertEqual(array('bootstrap' => false) + $app, $result);
}
/**
@@ -130,11 +152,48 @@ class LibrariesTest extends \lithium\test\Unit {
* @return void
*/
public function testAddInvalidLibrary() {
- $this->expectException("Library 'invalid_foo' not found.");
+ $this->expectException("Library `invalid_foo` not found.");
Libraries::add('invalid_foo');
}
/**
+ * Tests that non-prefixed (poorly named or structured) libraries can still be added.
+ *
+ * @return void
+ */
+ public function testAddNonPrefixedLibrary() {
+ $tmpDir = realpath(Libraries::get(true, 'resources') . '/tmp');
+ $this->skipIf(!is_writable($tmpDir), "Can't write to resources directory.");
+
+ $fakeDir = $tmpDir . '/fake';
+ $fake = "<?php class Fake {} ?>";
+ $fakeFilename = $fakeDir . '/fake.php';
+ mkdir($fakeDir);
+ file_put_contents($fakeFilename, $fake);
+
+ Libraries::add('bad', array(
+ 'prefix' => false,
+ 'path' => $fakeDir,
+ 'transform' => function($class, $config) { return ''; }
+ ));
+
+ Libraries::add('fake', array(
+ 'path' => $fakeDir,
+ 'includePath' => true,
+ 'prefix' => false,
+ 'transform' => function($class, $config) {
+ return $config['path'] . '/' . Inflector::underscore($class) . '.php';
+ }
+ ));
+
+ $this->assertFalse(class_exists('Fake', false));
+ $this->assertTrue(class_exists('Fake'));
+ unlink($fakeFilename);
+ rmdir($fakeDir);
+ Libraries::remove('fake');
+ }
+
+ /**
* Tests that non-class files are always filtered out of `find()` results unless an alternate
* filter is specified.
*
@@ -170,7 +229,7 @@ class LibrariesTest extends \lithium\test\Unit {
* @return void
*/
public function testLibraryLoad() {
- $this->expectException('Failed to load SomeInvalidLibrary from ');
+ $this->expectException('Failed to load class `SomeInvalidLibrary` from path ``.');
Libraries::load('SomeInvalidLibrary', true);
}
@@ -188,6 +247,16 @@ class LibrariesTest extends \lithium\test\Unit {
$this->assertEqual(realpath($result[__CLASS__]), __FILE__);
}
+ public function testCacheControl() {
+ $this->assertNull(Libraries::path('Foo'));
+ $cache = Libraries::cache();
+ Libraries::cache(array('Foo' => 'Bar'));
+ $this->assertEqual('Bar', Libraries::path('Foo'));
+
+ Libraries::cache(false);
+ Libraries::cache($cache);
+ }
+
/**
* Tests recursive and non-recursive searching through libraries with paths.
*
@@ -226,15 +295,20 @@ class LibrariesTest extends \lithium\test\Unit {
}
public function testFindingClassesWithExclude() {
- $expected = array();
$options = array(
'recursive' => true,
'filter' => false,
'exclude' => '/\w+Test$|webroot|index$|^app\\\\config|^\w+\\\\views\/|\./'
);
$classes = Libraries::find('lithium', $options);
- $result = preg_grep('/\w+Test/', $classes);
- $this->assertEqual($expected, $result);
+
+ $this->assertTrue(in_array('lithium\util\Set', $classes));
+ $this->assertTrue(in_array('lithium\util\Collection', $classes));
+ $this->assertTrue(in_array('lithium\core\Libraries', $classes));
+ $this->assertTrue(in_array('lithium\action\Dispatcher', $classes));
+
+ $this->assertFalse(in_array('lithium\tests\integration\data\SourceTest', $classes));
+ $this->assertFalse(preg_grep('/\w+Test$/', $classes));
$expected = Libraries::find('lithium', array(
'filter' => '/\w+Test$/', 'recursive' => true
@@ -248,10 +322,25 @@ class LibrariesTest extends \lithium\test\Unit {
$this->assertTrue(count($result) > 30);
$expected = array(
- 'lithium\template\view\adapter\File', 'lithium\template\view\adapter\Simple'
+ 'lithium\template\view\adapter\File',
+ 'lithium\template\view\adapter\Simple'
);
$result = Libraries::locate('adapter.template.view');
$this->assertEqual($expected, $result);
+
+ $result = Libraries::locate('test.filter');
+ $this->assertTrue(count($result) >= 4);
+ $this->assertTrue(in_array('lithium\test\filter\Affected', $result));
+ $this->assertTrue(in_array('lithium\test\filter\Complexity', $result));
+ $this->assertTrue(in_array('lithium\test\filter\Coverage', $result));
+ $this->assertTrue(in_array('lithium\test\filter\Profiler', $result));
+ }
+
+ public function testServiceLocateInstantiation() {
+ $result = Libraries::instance('adapter.template.view', 'Simple');
+ $this->assertTrue(is_a($result, 'lithium\template\view\adapter\Simple'));
+ $this->expectException("Class `Foo` of type `adapter.template.view` not found.");
+ $result = Libraries::instance('adapter.template.view', 'Foo');
}
public function testServiceLocateAllCommands() {
@@ -293,7 +382,7 @@ class LibrariesTest extends \lithium\test\Unit {
$expected = '\lithium\data\source\Database';
$this->assertEqual($expected, $result);
- $expected = new \stdClass();
+ $expected = new stdClass();
$result = Libraries::locate(null, $expected);
$this->assertEqual($expected, $result);
}
@@ -319,13 +408,6 @@ class LibrariesTest extends \lithium\test\Unit {
$library = Libraries::get('lithium');
$base = $library['path'] . '/';
- $expected = $base . 'template/view.php';
- $result = Libraries::path('\lithium\template\view');
- $this->assertEqual($expected, $result);
-
- $result = Libraries::path('lithium\template\view');
- $this->assertEqual($expected, $result);
-
$expected = $base . 'template/View.php';
$result = Libraries::path('\lithium\template\View');
@@ -357,11 +439,10 @@ class LibrariesTest extends \lithium\test\Unit {
public function testFindingClassesWithCallableFilters() {
$result = Libraries::find('lithium', array(
- 'recursive' => true, 'path' => '/tests/cases',
- 'format' => function($file, $config) {
+ 'recursive' => true, 'path' => '/tests/cases', 'format' => function($file, $config) {
return new SplFileInfo($file);
},
- 'filter' => function($file) {
+ 'filter' => function($file) {
if ($file->getFilename() === 'LibrariesTest.php') {
return $file;
}
@@ -391,15 +472,18 @@ class LibrariesTest extends \lithium\test\Unit {
public function testFindWithOptions() {
$result = Libraries::find('lithium', array(
'path' => '/console/command/create/template',
- 'namespaces' => false, 'suffix' => false,
- 'filter' => false, 'exclude' => false, 'format' => function ($file, $config) {
+ 'namespaces' => false,
+ 'suffix' => false,
+ 'filter' => false,
+ 'exclude' => false,
+ 'format' => function ($file, $config) {
return basename($file);
- },
+ }
));
$this->assertTrue(count($result) > 3);
- $this->assertTrue(array_search('controller.txt.php', $result));
- $this->assertTrue(array_search('model.txt.php', $result));
- $this->assertTrue(array_search('plugin.phar.gz', $result));
+ $this->assertTrue(array_search('controller.txt.php', $result) !== false);
+ $this->assertTrue(array_search('model.txt.php', $result) !== false);
+ $this->assertTrue(array_search('plugin.phar.gz', $result) !== false);
}
public function testLocateWithDotSyntax() {
@@ -407,6 +491,94 @@ class LibrariesTest extends \lithium\test\Unit {
$result = Libraries::locate('controllers', 'app.Pages');
$this->assertEqual($expected, $result);
}
+
+ public function testLocateCommandInLithium() {
+ $expected = array(
+ 'lithium\console\command\Create',
+ 'lithium\console\command\G11n',
+ 'lithium\console\command\Help',
+ 'lithium\console\command\Library',
+ 'lithium\console\command\Route',
+ 'lithium\console\command\Test'
+ );
+ $result = Libraries::locate('command', null, array(
+ 'library' => 'lithium', 'recursive' => false
+ ));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testLocateCommandInLithiumRecursiveTrue() {
+ $expected = array(
+ 'lithium\console\command\Create',
+ 'lithium\console\command\G11n',
+ 'lithium\console\command\Help',
+ 'lithium\console\command\Library',
+ 'lithium\console\command\Route',
+ 'lithium\console\command\Test',
+ 'lithium\console\command\g11n\Extract',
+ 'lithium\console\command\create\Controller',
+ 'lithium\console\command\create\Mock',
+ 'lithium\console\command\create\Model',
+ 'lithium\console\command\create\Test',
+ 'lithium\console\command\create\View'
+ );
+ $result = Libraries::locate('command', null, array(
+ 'library' => 'lithium', 'recursive' => true
+ ));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testLocateWithLibrary() {
+ $expected = array();
+ $result = (array) Libraries::locate("tests", null, array('library' => 'doesntExist'));
+ $this->assertIdentical($expected, $result);
+ }
+
+ public function testLocateWithLithiumLibrary() {
+ $expected = (array) Libraries::find('lithium', array(
+ 'path' => '/tests',
+ 'preFilter' => '/[A-Z][A-Za-z0-9]+\Test\./',
+ 'recursive' => true,
+ 'filter' => '/cases|integration|functional|mocks/',
+ ));
+ $result = (array) Libraries::locate("tests", null, array('library' => 'lithium'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testLocateWithTestAppLibrary() {
+ $testApp = Libraries::get(true, 'resources') . '/tmp/tests/test_app';
+ mkdir($testApp);
+ Libraries::add('test_app', array('path' => $testApp));
+
+ mkdir($testApp . '/tests/cases/models', 0777, true);
+ file_put_contents($testApp . '/tests/cases/models/UserTest.php',
+ "<?php namespace test_app\\tests\\cases\\models;\n
+ class UserTest extends \\lithium\\test\\Unit { public function testMe() {
+ \$this->assertTrue(true);
+ }}"
+ );
+ Libraries::cache(false);
+
+ $expected = array('test_app\\tests\\cases\\models\\UserTest');
+ $result = (array) Libraries::locate("tests", null, array('library' => 'test_app'));
+ $this->assertEqual($expected, $result);
+
+ $this->_cleanUp();
+ }
+
+ /**
+ * Tests that `Libraries::realPath()` correctly resolves paths to files inside Phar archives.
+ *
+ * @return void
+ */
+ public function testPathsInPharArchives() {
+ $base = Libraries::get('lithium', 'path');
+ $path = "{$base}/console/command/create/template/app.phar.gz";
+
+ $expected = "phar://{$path}/controllers/HelloWorldController.php";
+ $result = Libraries::realPath($expected);
+ $this->assertEqual($expected, $result);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/ObjectTest.php b/libraries/lithium/tests/cases/core/ObjectTest.php
index 139e75c..954dd4b 100644
--- a/libraries/lithium/tests/cases/core/ObjectTest.php
+++ b/libraries/lithium/tests/cases/core/ObjectTest.php
@@ -2,18 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\core;
-use \lithium\core\Object;
-use \lithium\tests\mocks\core\MockMethodFiltering;
-use \lithium\tests\mocks\core\MockExposed;
-use \lithium\tests\mocks\core\MockCallable;
-use \lithium\tests\mocks\core\MockObjectForParents;
-use \lithium\tests\mocks\core\MockObjectConfiguration;
+use lithium\core\Object;
+use lithium\tests\mocks\core\MockRequest;
+use lithium\tests\mocks\core\MockMethodFiltering;
+use lithium\tests\mocks\core\MockExposed;
+use lithium\tests\mocks\core\MockCallable;
+use lithium\tests\mocks\core\MockObjectForParents;
+use lithium\tests\mocks\core\MockObjectConfiguration;
+use lithium\tests\mocks\core\MockInstantiator;
class ObjectTest extends \lithium\test\Unit {
@@ -175,6 +177,34 @@ class ObjectTest extends \lithium\test\Unit {
$this->assertEqual('test', $result->getProtected());
}
+
+ public function testInstanceWithClassesKey() {
+ $object = new MockInstantiator();
+ $expected = 'lithium\tests\mocks\core\MockRequest';
+ $result = get_class($object->instance('request'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInstanceWithNamespacedClass() {
+ $object = new MockInstantiator();
+ $expected = 'lithium\tests\mocks\core\MockRequest';
+ $result = get_class($object->instance('lithium\tests\mocks\core\MockRequest'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInstanceWithObject() {
+ $object = new MockInstantiator();
+ $request = new MockRequest();
+ $expected = 'lithium\tests\mocks\core\MockRequest';
+ $result = get_class($object->instance($request));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInstanceFalse() {
+ $object = new MockInstantiator();
+ $this->expectException('/^Invalid class lookup/');
+ $object->instance(false);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/StaticObjectTest.php b/libraries/lithium/tests/cases/core/StaticObjectTest.php
index fb9b775..18cca3e 100644
--- a/libraries/lithium/tests/cases/core/StaticObjectTest.php
+++ b/libraries/lithium/tests/cases/core/StaticObjectTest.php
@@ -2,13 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\core;
-use \lithium\core\StaticObject;
+use lithium\core\StaticObject;
+use lithium\tests\mocks\core\MockRequest;
+use lithium\tests\mocks\core\MockStaticInstantiator;
class StaticObjectTest extends \lithium\test\Unit {
@@ -119,9 +121,47 @@ class StaticObjectTest extends \lithium\test\Unit {
* @return void
*/
public function testCallingSubclassMethodsInFilteredMethods() {
- $class = '\lithium\tests\mocks\core\MockStaticFilteringExtended';
+ $class = 'lithium\tests\mocks\core\MockStaticFilteringExtended';
$this->assertEqual('Working', $class::callSubclassMethod());
}
+
+ public function testClassParents() {
+ $class = 'lithium\tests\mocks\core\MockStaticMethodFiltering';
+ $class::parents(null);
+
+ $result = $class::parents();
+ $expected = array('lithium\core\StaticObject' => 'lithium\core\StaticObject');
+ $this->assertEqual($expected, $result);
+
+ $cache = $class::parents(true);
+ $this->assertEqual(array($class => $expected), $cache);
+ }
+
+ public function testInstanceWithClassesKey() {
+ $expected = 'lithium\tests\mocks\core\MockRequest';
+ $result = get_class(MockStaticInstantiator::instance('request'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInstanceWithNamespacedClass() {
+ $expected = 'lithium\tests\mocks\core\MockRequest';
+ $result = get_class(MockStaticInstantiator::instance(
+ 'lithium\tests\mocks\core\MockRequest'
+ ));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInstanceWithObject() {
+ $request = new MockRequest();
+ $expected = 'lithium\tests\mocks\core\MockRequest';
+ $result = get_class(MockStaticInstantiator::instance($request));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInstanceFalse() {
+ $this->expectException('/^Invalid class lookup/');
+ MockStaticInstantiator::instance(false);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/CollectionTest.php b/libraries/lithium/tests/cases/data/CollectionTest.php
index 22ef792..d4ced38 100644
--- a/libraries/lithium/tests/cases/data/CollectionTest.php
+++ b/libraries/lithium/tests/cases/data/CollectionTest.php
@@ -2,14 +2,107 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
- *
*/
namespace lithium\tests\cases\data;
+use lithium\data\collection\DocumentSet;
+
class CollectionTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\model\MockQueryPost';
+
+ public function testGetStats() {
+ $collection = new DocumentSet(array('stats' => array('foo' => 'bar')));
+ $this->assertNull($collection->stats('bar'));
+ $this->assertEqual('bar', $collection->stats('foo'));
+ $this->assertEqual(array('foo' => 'bar'), $collection->stats());
+ }
+
+ public function testInvalidData() {
+ $this->expectException('Error creating new Collection instance; data format invalid.');
+ $collection = new DocumentSet(array('data' => 'foo'));
+ }
+
+ public function testAccessorMethods() {
+ $model = $this->_model;
+ $model::config(array('connection' => false));
+ $collection = new DocumentSet(compact('model'));
+ $this->assertEqual($model, $collection->model());
+ $this->assertEqual(compact('model'), $collection->meta());
+ }
+
+ public function testOffsetExists() {
+ $collection = new DocumentSet();
+ $this->assertEqual($collection->offsetExists(0), false);
+ $collection->set(array('foo' => 'bar', 'bas' => 'baz'));
+ $this->assertEqual($collection->offsetExists(0), true);
+ $this->assertEqual($collection->offsetExists(1), true);
+ }
+
+ public function testNextRewindCurrent() {
+ $collection = new DocumentSet();
+ $collection->set(array(
+ 'title' => 'Lorem Ipsum',
+ 'value' => 42,
+ 'foo' => 'bar'
+ ));
+ $this->assertEqual('Lorem Ipsum', $collection->current());
+ $this->assertEqual(42, $collection->next());
+ $this->assertEqual('bar', $collection->next());
+ $this->assertEqual('Lorem Ipsum', $collection->rewind());
+ $this->assertEqual(42, $collection->next());
+ }
+
+ public function testEach() {
+ $collection = new DocumentSet();
+ $collection->set(array(
+ 'title' => 'Lorem Ipsum',
+ 'key' => 'value',
+ 'foo' => 'bar'
+ ));
+ $collection->each(function($value) {
+ return $value . ' test';
+ });
+ $expected = array(
+ 'Lorem Ipsum test',
+ 'value test',
+ 'bar test'
+ );
+ $this->assertEqual($collection->to('array'), $expected);
+ }
+
+ public function testMap() {
+ $collection = new DocumentSet();
+ $collection->set(array(
+ 'title' => 'Lorem Ipsum',
+ 'key' => 'value',
+ 'foo' => 'bar'
+ ));
+ $results = $collection->map(function($value) {
+ return $value . ' test';
+ });
+ $expected = array(
+ 'Lorem Ipsum test',
+ 'value test',
+ 'bar test'
+ );
+ $this->assertEqual($results->to('array'), $expected);
+ $this->assertNotEqual($results->to('array'), $collection->to('array'));
+ }
+
+ public function testData() {
+ $collection = new DocumentSet();
+ $data = array(
+ 'Lorem Ipsum',
+ 'value',
+ 'bar'
+ );
+ $collection->set($data);
+ $this->assertEqual($data, $collection->data());
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/ConnectionsTest.php b/libraries/lithium/tests/cases/data/ConnectionsTest.php
index 32e58d2..3830b27 100644
--- a/libraries/lithium/tests/cases/data/ConnectionsTest.php
+++ b/libraries/lithium/tests/cases/data/ConnectionsTest.php
@@ -2,20 +2,25 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data;
-use \lithium\data\Connections;
+use Exception;
+use lithium\data\Connections;
+use lithium\data\source\Http;
+use lithium\data\source\Mock;
+use lithium\data\source\database\adapter\MySql;
class ConnectionsTest extends \lithium\test\Unit {
public $config = array(
- 'adapter' => 'MySql',
- 'host' => 'localhost',
- 'login' => '--user--',
+ 'type' => 'database',
+ 'adapter' => 'MySql',
+ 'host' => 'localhost',
+ 'login' => '--user--',
'password' => '--pass--',
'database' => 'db'
);
@@ -42,20 +47,23 @@ class ConnectionsTest extends \lithium\test\Unit {
$expected = $this->config + array('type' => 'database');
$this->assertEqual($expected, $result);
+ $this->skipIf(!MySql::enabled(), 'MySql is not enabled');
+ $this->skipIf(!$this->_canConnect('localhost', 3306), 'Cannot connect to localhost:3306');
+
$this->expectException('/mysql_get_server_info/');
$this->expectException('/mysql_select_db/');
- $this->expectException('/mysql_connect/');
+ $this->expectException('/mysql_pconnect/');
$result = Connections::get('conn-test');
- $this->assertTrue($result instanceof \lithium\data\source\database\adapter\MySql);
+ $this->assertTrue($result instanceof MySql);
$result = Connections::add('conn-test-2', $this->config);
$this->assertEqual($expected, $result);
$this->expectException('/mysql_get_server_info/');
$this->expectException('/mysql_select_db/');
- $this->expectException('/mysql_connect/');
+ $this->expectException('/mysql_pconnect/');
$result = Connections::get('conn-test-2');
- $this->assertTrue($result instanceof \lithium\data\source\database\adapter\MySql);
+ $this->assertTrue($result instanceof MySql);
}
public function testConnectionGetAndReset() {
@@ -63,6 +71,9 @@ class ConnectionsTest extends \lithium\test\Unit {
Connections::add('conn-test-2', $this->config);
$this->assertEqual(array('conn-test', 'conn-test-2'), Connections::get());
+ $this->skipIf(!MySql::enabled(), 'MySql is not enabled');
+ $this->skipIf(!$this->_canConnect('localhost', 3306), 'Cannot connect to localhost:3306');
+
$expected = $this->config + array('type' => 'database', 'filters' => array());
$this->assertEqual($expected, Connections::get('conn-test', array('config' => true)));
@@ -76,14 +87,17 @@ class ConnectionsTest extends \lithium\test\Unit {
Connections::add('conn-test', $this->config);
Connections::add('conn-test-2', $this->config);
+ $this->skipIf(!MySql::enabled(), 'MySql is not enabled');
+ $this->skipIf(!$this->_canConnect('localhost', 3306), 'Cannot connect to localhost:3306');
+
$this->expectException('/mysql_get_server_info/');
$this->expectException('/mysql_select_db/');
- $this->expectException('/mysql_connect/');
+ $this->expectException('/mysql_pconnect/');
$result = Connections::get('conn-test');
- $this->assertTrue($result instanceof \lithium\data\source\database\adapter\MySql);
+ $this->assertTrue($result instanceof MySql);
$result = Connections::get('conn-test');
- $this->assertTrue($result instanceof \lithium\data\source\database\adapter\MySql);
+ $this->assertTrue($result instanceof MySql);
$this->assertNull(Connections::get('conn-test-2', array('autoCreate' => false)));
}
@@ -104,17 +118,18 @@ class ConnectionsTest extends \lithium\test\Unit {
Connections::add('stream-test', $config);
$result = Connections::get('stream-test');
- $this->assertTrue($result instanceof \lithium\data\source\Http);
+ $this->assertTrue($result instanceof Http);
+ Connections::config(array('stream-test' => false));
}
-/*
public function testErrorExceptions() {
$config = array(
'adapter' => 'None',
'type' => 'Error'
);
- Connections::add('NoConnection', 'Error', $config);
+ Connections::add('NoConnection', $config);
$result = false;
+
try {
Connections::get('NoConnection');
} catch(Exception $e) {
@@ -122,7 +137,25 @@ class ConnectionsTest extends \lithium\test\Unit {
}
$this->assertTrue($result, 'Exception is not thrown');
}
-*/
+
+ public function testGetNullAdapter() {
+ Connections::reset();
+ $this->assertTrue(Connections::get(false) instanceof Mock);
+ }
+
+ protected function _canConnect($host, $port) {
+ $this->expectException();
+ $this->expectException();
+
+ if ($conn = fsockopen($host, $port)) {
+ array_pop($this->_expected);
+ array_pop($this->_expected);
+ fclose($conn);
+
+ return true;
+ }
+ return false;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/EntityTest.php b/libraries/lithium/tests/cases/data/EntityTest.php
new file mode 100644
index 0000000..35ec22a
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/EntityTest.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\data;
+
+use lithium\data\Entity;
+
+class EntityTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\source\MockMongoPost';
+
+ public function testSchemaAccess() {
+ $schema = array('foo' => array('type' => 'string'));
+ $entity = new Entity(compact('schema'));
+ $this->assertEqual($schema, $entity->schema());
+ }
+
+ public function testPropertyAccess() {
+ $entity = new Entity(array('model' => 'Foo', 'exists' => false));
+ $this->assertEqual('Foo', $entity->model());
+ $this->assertFalse($entity->exists());
+
+ $entity = new Entity(array('exists' => true));
+ $this->assertTrue($entity->exists());
+
+ $expected = array(
+ 'exists' => true, 'data' => array(), 'update' => array(), 'increment' => array()
+ );
+ $this->assertEqual($expected, $entity->export());
+ }
+
+ public function testIncrement() {
+ $entity = new Entity(array('data' => array('counter' => 0)));
+ $this->assertEqual(0, $entity->counter);
+
+ $entity->increment('counter');
+ $this->assertEqual(1, $entity->counter);
+
+ $entity->decrement('counter', 5);
+ $this->assertEqual(-4, $entity->counter);
+
+ $this->assertNull($entity->increment);
+ $entity->increment('foo');
+ $this->assertEqual(1, $entity->foo);
+
+ $this->assertFalse(isset($entity->bar));
+ $entity->bar = 'blah';
+ $entity->update();
+
+ $this->expectException("/^Field 'bar' cannot be incremented.$/");
+ $entity->increment('bar');
+ }
+
+ public function testMethodDispatch() {
+ $entity = new Entity(array('model' => $this->_model, 'data' => array('foo' => true)));
+ $this->assertTrue($entity->validates());
+ $this->expectException("/^No model bound or unhandled method call `foo`.$/");
+ $entity->foo();
+ }
+
+ public function testErrors() {
+ $entity = new Entity();
+ $errors = array('foo' => 'Something bad happened.');
+ $this->assertEqual(array(), $entity->errors());
+
+ $entity->errors($errors);
+ $this->assertEqual($errors, $entity->errors());
+ $this->assertEqual('Something bad happened.', $entity->errors('foo'));
+ }
+
+ public function testConversion() {
+ $data = array('foo' => '!!', 'bar' => '??', 'baz' => '--');
+ $entity = new Entity(compact('data'));
+
+ $this->assertEqual($data, $entity->to('array'));
+ $this->assertEqual($data, $entity->data());
+ $this->assertEqual($entity, $entity->to('foo'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/ModelTest.php b/libraries/lithium/tests/cases/data/ModelTest.php
index 0f5c02d..3d6430b 100644
--- a/libraries/lithium/tests/cases/data/ModelTest.php
+++ b/libraries/lithium/tests/cases/data/ModelTest.php
@@ -2,26 +2,51 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data;
-use \lithium\data\Model;
-use \lithium\data\Connections;
-use \lithium\analysis\Inspector;
-use \lithium\tests\mocks\data\MockPost;
-use \lithium\tests\mocks\data\MockComment;
-use \lithium\tests\mocks\data\MockTag;
-use \lithium\tests\mocks\data\MockTagging;
-use \lithium\tests\mocks\data\MockCreator;
-use \lithium\tests\mocks\data\MockPostForValidates;
+use lithium\data\Model;
+use lithium\data\Entity;
+use lithium\data\model\Query;
+use lithium\data\Connections;
+use lithium\analysis\Inspector;
+use lithium\data\entity\Record;
+use lithium\tests\mocks\data\MockTag;
+use lithium\tests\mocks\data\MockPost;
+use lithium\tests\mocks\data\MockSource;
+use lithium\tests\mocks\data\MockComment;
+use lithium\tests\mocks\data\MockTagging;
+use lithium\tests\mocks\data\MockCreator;
+use lithium\tests\mocks\data\MockPostForValidates;
+use lithium\tests\mocks\data\source\MockMongoConnection;
class ModelTest extends \lithium\test\Unit {
- public function _init() {
- Connections::add('mock-source', array('type' => '\lithium\tests\mocks\data\MockSource'));
+ protected $_configs = array();
+
+ protected $_altSchema = array(
+ 'id' => array('type' => 'integer'),
+ 'author_id' => array('type' => 'integer'),
+ 'title' => array('type' => 'string'),
+ 'body' => array('type' => 'text')
+ );
+
+ public function setUp() {
+ $this->_configs = Connections::config();
+ Connections::config(array('mock-source' => array(
+ 'type' => 'lithium\tests\mocks\data\MockSource'
+ )));
+ MockPost::config(array('connection' => 'mock-source'));
+ MockTag::config();
+ MockComment::config();
+ }
+
+ public function tearDown() {
+ Connections::config(array('mock-source' => false));
+ Connections::config($this->_configs);
}
public function testOverrideMeta() {
@@ -42,22 +67,27 @@ class ModelTest extends \lithium\test\Unit {
public function testClassInitialization() {
$expected = MockPost::instances();
- MockPost::__init();
+ MockPost::config();
$this->assertEqual($expected, MockPost::instances());
- Model::__init();
+ Model::config();
$this->assertEqual($expected, MockPost::instances());
- $this->assertEqual('mock_posts', \lithium\tests\mocks\data\MockPost::meta('source'));
+ $this->assertEqual('mock_posts', MockPost::meta('source'));
- MockPost::__init(array('source' => 'post'));
+ MockPost::config(array('source' => 'post'));
$this->assertEqual('post', MockPost::meta('source'));
- MockPost::__init(array('source' => false));
+ MockPost::config(array('source' => false));
$this->assertIdentical(false, MockPost::meta('source'));
- MockPost::__init(array('source' => null));
+ MockPost::config(array('source' => null));
$this->assertIdentical('mock_posts', MockPost::meta('source'));
+
+ MockPost::config();
+ $this->assertEqual('mock_posts', MockPost::meta('source'));
+
+ $this->assertEqual('mock-source', MockPost::meta('connection'));
}
public function testMetaInformation() {
@@ -68,9 +98,10 @@ class ModelTest extends \lithium\test\Unit {
'title' => 'title',
'source' => 'mock_posts',
'connection' => 'mock-source',
- 'initialized' => true
+ 'initialized' => true,
+ 'locked' => true,
);
- MockPost::__init();
+ MockPost::config();
$this->assertEqual($expected, MockPost::meta());
$expected = array(
@@ -80,7 +111,8 @@ class ModelTest extends \lithium\test\Unit {
'title' => 'comment_id',
'source' => 'mock_comments',
'connection' => 'mock-source',
- 'initialized' => true
+ 'initialized' => true,
+ 'locked' => true,
);
$this->assertEqual($expected, MockComment::meta());
@@ -113,14 +145,14 @@ class ModelTest extends \lithium\test\Unit {
* @return void
*/
public function testRelationshipIntrospection() {
- $result = MockPost::relations();
+ $result = array_keys(MockPost::relations());
$expected = array('MockComment');
$this->assertEqual($expected, $result);
$result = MockPost::relations('hasMany');
$this->assertEqual($expected, $result);
- $result = MockComment::relations();
+ $result = array_keys(MockComment::relations());
$expected = array('MockPost');
$this->assertEqual($expected, $result);
@@ -130,8 +162,33 @@ class ModelTest extends \lithium\test\Unit {
$this->assertFalse(MockComment::relations('hasMany'));
$this->assertFalse(MockPost::relations('belongsTo'));
- $this->assertNull(MockComment::relations('MockPost'));
- $this->assertNull(MockPost::relations('MockComment'));
+ $expected = array(
+ 'name' => 'MockPost',
+ 'type' => 'belongsTo',
+ 'keys' => array('mock_post_id' => 'id'),
+ 'from' => 'lithium\tests\mocks\data\MockComment',
+ 'to' => 'lithium\tests\mocks\data\MockPost',
+ 'link' => 'key',
+ 'fields' => true,
+ 'fieldName' => 'mockPost',
+ 'constraint' => array(),
+ 'init' => true
+ );
+ $this->assertEqual($expected, MockComment::relations('MockPost')->data());
+
+ $expected = array(
+ 'name' => 'MockComment',
+ 'type' => 'hasMany',
+ 'from' => 'lithium\tests\mocks\data\MockPost',
+ 'to' => 'lithium\tests\mocks\data\MockComment',
+ 'fields' => true,
+ 'keys' => array('mock_post_id' => 'id'),
+ 'link' => 'key',
+ 'fieldName' => 'mockComment',
+ 'constraint' => array(),
+ 'init' => true
+ );
+ $this->assertEqual($expected, MockPost::relations('MockComment')->data());
}
public function testSimpleRecordCreation() {
@@ -145,11 +202,35 @@ class ModelTest extends \lithium\test\Unit {
$expected = 'Do you ever read any of the books you burn?';
$this->assertEqual($expected, $comment->text);
+
+ $comment = MockComment::create(
+ array('author_id' => 111, 'text' => 'This comment should already exist'),
+ array('exists' => true)
+ );
+ $this->assertTrue($comment->exists());
}
public function testSimpleFind() {
$result = MockPost::find('all');
- $this->assertTrue($result['query'] instanceof \lithium\data\model\Query);
+ $this->assertTrue($result['query'] instanceof Query);
+ }
+
+ public function testMagicFinders() {
+ $result = MockPost::findById(5);
+ $result2 = MockPost::findFirstById(5);
+ $this->assertEqual($result2, $result);
+
+ $expected = array('id' => 5);
+ $this->assertEqual($expected, $result['query']->conditions());
+ $this->assertEqual('read', $result['query']->type());
+
+ $result = MockPost::findAllByFoo(13, array('order' => array('created_at' => 'desc')));
+ $this->assertFalse($result['query']->data());
+ $this->assertEqual(array('foo' => 13), $result['query']->conditions());
+ $this->assertEqual(array('created_at' => 'desc'), $result['query']->order());
+
+ $this->expectException('/Method `findFoo` not defined or handled in class/');
+ MockPost::findFoo();
}
/**
@@ -159,15 +240,22 @@ class ModelTest extends \lithium\test\Unit {
*/
public function testSimpleFindFirst() {
$result = MockComment::first();
- $this->assertTrue($result instanceof \lithium\data\model\Record);
+ $this->assertTrue($result instanceof Record);
$expected = 'First comment';
$this->assertEqual($expected, $result->text);
}
+ public function testSimpleFindList() {
+ $result = MockComment::find('list');
+ $this->assertTrue(!empty($result));
+ $this->assertTrue(is_array($result));
+ }
+
public function testFilteredFind() {
MockComment::applyFilter('find', function($self, $params, $chain) {
$result = $chain->next($self, $params, $chain);
+
if ($result != null) {
$result->filtered = true;
}
@@ -184,7 +272,10 @@ class ModelTest extends \lithium\test\Unit {
}
public function testCustomFindMethods() {
- print_r(MockPost::findFirstById());
+ $result = MockPost::findFirstById(5);
+ $query = $result['query'];
+ $this->assertEqual(array('id' => 5), $query->conditions());
+ $this->assertEqual(1, $query->limit());
}
public function testKeyGeneration() {
@@ -192,13 +283,15 @@ class ModelTest extends \lithium\test\Unit {
$this->assertEqual(array('post_id', 'tag_id'), MockTagging::key());
$result = MockComment::key(array('comment_id' => 5, 'body' => 'This is a comment'));
- $this->assertEqual(5, $result);
+ $this->assertEqual(array('comment_id' => 5), $result);
$result = MockTagging::key(array(
'post_id' => 2,
'tag_id' => 5,
'created' => '2009-06-16 10:00:00'
));
+ $this->assertEqual('id', MockPost::key());
+ $this->assertEqual(array('id' => 5), MockPost::key(5));
$this->assertEqual(array('post_id' => 2, 'tag_id' => 5), $result);
}
@@ -258,6 +351,22 @@ class ModelTest extends \lithium\test\Unit {
$this->assertTrue(empty($result));
}
+ public function testCustomValidationCriteria() {
+ $validates = array(
+ 'title' => 'A custom message here for empty titles.',
+ 'email' => array(
+ array('notEmpty', 'message' => 'email is empty.'),
+ )
+ );
+ $post = MockPostForValidates::create(array(
+ 'title' => 'custom validation', 'email' => 'asdf'
+ ));
+
+ $result = $post->validates(array('rules' => $validates));
+ $this->assertTrue($result === true);
+ $this->assertIdentical(array(), $post->errors());
+ }
+
public function testDefaultValuesFromSchema() {
$creator = MockCreator::create();
$expected = array(
@@ -295,42 +404,143 @@ class ModelTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
- /*
- * @todo create proper mock objects for the following test
- *
- public function testFindAll() {
- $tags = MockTag::find('all', array('conditions' => array('id' => 2)));
+ public function testModelWithNoBackend() {
+ $this->assertEqual('mock-source', MockPost::meta('connection'));
+ MockPost::config(array('connection' => false));
+ $this->assertFalse(MockPost::meta('connection'));
+ $schema = MockPost::schema();
- $this->assertTrue($tags instanceof \lithium\data\collection\RecordSet);
- $this->assertEqual(1, $tags->count());
- $tag = $tags->rewind();
- $this->assertTrue($tag instanceof \lithium\data\model\Record);
+ MockPost::overrideSchema($this->_altSchema);
+ $this->assertEqual($this->_altSchema, MockPost::schema());
- $tags2 = MockTag::find('all', array('conditions' => array('id' => 3)));
+ $post = MockPost::create(array('title' => 'New post'));
+ $this->assertTrue($post instanceof Entity);
+ $this->assertEqual('New post', $post->title);
+ MockPost::overrideSchema($schema);
- $this->assertEqual(0, $tags2->count());
+ $this->expectException('/Connection name not defined/');
+ $post->save();
+ }
+
+ public function testSave() {
+ $schema = MockPost::schema();
+ MockPost::overrideSchema($this->_altSchema);
+ $data = array('title' => 'New post', 'author_id' => 13);
+ $record = MockPost::create($data);
+ $result = $record->save();
+
+ $this->assertEqual('create', $result['query']->type());
+ $this->assertEqual($data, $result['query']->data());
+ $this->assertEqual('lithium\tests\mocks\data\MockPost', $result['query']->model());
+ MockPost::overrideSchema($schema);
+ }
+
+ public function testSaveWithNoCallbacks() {
+ $schema = MockPost::schema();
+ MockPost::overrideSchema($this->_altSchema);
+ $data = array('title' => 'New post', 'author_id' => 13);
+ $record = MockPost::create($data);
+ $result = $record->save(null, array('callbacks' => false));
+
+ $this->assertEqual('create', $result['query']->type());
+ $this->assertEqual($data, $result['query']->data());
+ $this->assertEqual('lithium\tests\mocks\data\MockPost', $result['query']->model());
+ MockPost::overrideSchema($schema);
+ }
+
+ public function testSaveWithFailedValidation() {
+ $data = array('title' => '', 'author_id' => 13);
+ $record = MockPost::create($data);
+ $result = $record->save(null, array('validate' => array(
+ 'title' => 'A title must be present'
+ )));
+
+ $this->assertIdentical(false, $result);
+ }
+
+ public function testImplicitKeyFind() {
+ $result = MockPost::find(10);
+ $this->assertEqual('read', $result['query']->type());
+ $this->assertEqual('lithium\tests\mocks\data\MockPost', $result['query']->model());
+ $this->assertEqual(array('id' => 10), $result['query']->conditions());
+ }
+
+ public function testDelete() {
+ $record = MockPost::create(array('id' => 5), array('exists' => true));
+ $result = $record->delete();
+ $this->assertEqual('delete', $result['query']->type());
+ $this->assertEqual('mock_posts', $result['query']->source());
+ $this->assertEqual(array('id' => 5), $result['query']->conditions());
+ }
+
+ public function testMultiRecordUpdate() {
+ $result = MockPost::update(
+ array('published' => false),
+ array('expires' => array('>=' => '2010-05-13'))
+ );
+ $query = $result['query'];
+ $this->assertEqual('update', $query->type());
+ $this->assertEqual(array('published' => false), $query->data());
+ $this->assertEqual(array('expires' => array('>=' => '2010-05-13')), $query->conditions());
+ }
+
+ public function testMultiRecordDelete() {
+ $result = MockPost::remove(array('published' => false));
+ $query = $result['query'];
+ $this->assertEqual('delete', $query->type());
+ $this->assertEqual(array('published' => false), $query->conditions());
+
+ $keys = array_keys(array_filter($query->export(Connections::get('mock-source'))));
+ $this->assertEqual(array('type', 'name', 'conditions', 'model', 'source'), $keys);
}
public function testFindFirst() {
- $tag = MockTag::find('first', array('conditions' => array('id' => 2)));
+ $tag = MockTag::find('first', array('conditions' => array('id' => 2)));
+ $tag2 = MockTag::find(2);
+ $tag3 = MockTag::first(2);
- $this->assertTrue($tag instanceof \lithium\data\model\Record);
- $this->assertEqual('2', $tag->id);
+ $this->assertEqual($tag, $tag2);
+ $this->assertEqual($tag, $tag3);
+ }
- $tag2 = MockTag::find('first', array('conditions' => array('id' => 3)));
+ /**
+ * Tests that varying `count` syntaxes all produce the same query operation (i.e.
+ * `Model::count(...)`, `Model::find('count', ...)` etc).
+ *
+ * @return void
+ */
+ public function testCountSyntax() {
+ $base = MockPost::count(array('email' => 'foo@example.com'));
+ $query = $base['query'];
- $this->assertNull($tag2);
+ $this->assertEqual('read', $query->type());
+ $this->assertEqual('count', $query->calculate());
+ $this->assertEqual(array('email' => 'foo@example.com'), $query->conditions());
+
+ $result = MockPost::find('count', array('conditions' => array(
+ 'email' => 'foo@example.com'
+ )));
+ $this->assertEqual($query, $result['query']);
+
+ $result = MockPost::count(array('conditions' => array('email' => 'foo@example.com')));
+ $this->assertEqual($query, $result['query']);
+ }
- $tag = MockTag::find(2);
+ public function testSettingNestedObjectDefaults() {
+ $this->skipIf(!MockMongoConnection::enabled(), 'MongoDb not enabled.');
- $this->assertTrue($tag instanceof \lithium\data\model\Record);
- $this->assertEqual('2', $tag->id);
+ MockPost::$connection = new MockMongoConnection();
+ $schema = MockPost::schema();
- $tag2 = MockTag::find(3);
+ MockPost::overrideSchema($schema + array('nested.value' => array(
+ 'type' => 'string',
+ 'default' => 'foo'
+ )));
+ $this->assertEqual('foo', MockPost::create()->nested->value);
- $this->assertNull($tag2);
+ MockPost::overrideSchema($schema);
+ MockPost::$connection = null;
}
- */
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/SourceTest.php b/libraries/lithium/tests/cases/data/SourceTest.php
index 4c35549..e8c6e3f 100644
--- a/libraries/lithium/tests/cases/data/SourceTest.php
+++ b/libraries/lithium/tests/cases/data/SourceTest.php
@@ -2,13 +2,50 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data;
+use lithium\data\Entity;
+use lithium\tests\mocks\data\MockSource;
+
class SourceTest extends \lithium\test\Unit {
+
+ public function testMethods() {
+ $source = new MockSource(array('autoConnect' => false));
+ $methods = $source->methods();
+ $expected = array(
+ 'connect', 'disconnect', 'entities', 'describe', 'create', 'read', 'update', 'delete',
+ 'schema', 'result', 'cast', 'relationship', 'calculation', '__construct', '__destruct',
+ '_init', 'isConnected', 'name', 'methods', 'configureClass', 'item', 'applyFilter',
+ 'invokeMethod', '__set_state', '_instance', '_filter', '_parents', '_stop'
+ );
+ $this->assertEqual($expected, $methods);
+ }
+
+ public function testBaseMethods() {
+ $source = new MockSource(array('autoConnect' => true));
+ $name = '{(\'Li\':"∆")}';
+ $this->assertEqual($name, $source->name($name));
+ $this->assertFalse($source->configureClass('Foo'));
+ }
+
+ public function testConnection() {
+ $source = new MockSource(array('autoConnect' => false));
+ $this->assertFalse($source->isConnected());
+ $this->assertTrue($source->isConnected(array('autoConnect' => true)));
+ $this->assertTrue($source->isConnected());
+ }
+
+ public function testItem() {
+ $source = new MockSource();
+ $entity = $source->item('Foo', array('foo' => 'bar'));
+ $this->assertTrue($entity instanceof Entity);
+ $this->assertEqual('Foo', $entity->model());
+ $this->assertEqual(array('foo' => 'bar'), $entity->data());
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/collection/DocumentArrayTest.php b/libraries/lithium/tests/cases/data/collection/DocumentArrayTest.php
new file mode 100644
index 0000000..0454b23
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/collection/DocumentArrayTest.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\data\collection;
+
+use lithium\data\source\MongoDb;
+use lithium\data\collection\DocumentArray;
+
+class DocumentArrayTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\model\MockDocumentPost';
+
+ public function testInitialCasting() {
+ $array = new DocumentArray(array(
+ 'model' => $this->_model,
+ 'pathKey' => 'foo.bar',
+ 'data' => array('5', '6', '7')
+ ));
+ foreach ($array as $value) {
+ $this->assertTrue(is_int($value));
+ }
+ }
+
+ public function testExport() {
+ $array = new DocumentArray(array(
+ 'model' => $this->_model,
+ 'pathKey' => 'foo.bar',
+ 'data' => array('5', '6', '7')
+ ));
+ $array[] = 8;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/collection/DocumentSetTest.php b/libraries/lithium/tests/cases/data/collection/DocumentSetTest.php
new file mode 100644
index 0000000..7fdf35a
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/collection/DocumentSetTest.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\data\collection;
+
+use stdClass;
+use lithium\data\Connections;
+use lithium\data\source\MongoDb;
+use lithium\data\source\http\adapter\CouchDb;
+use lithium\data\entity\Document;
+use lithium\data\collection\DocumentSet;
+use lithium\data\collection\DocumentArray;
+use lithium\tests\mocks\data\model\MockDocumentPost;
+use lithium\tests\mocks\data\model\MockDocumentSource;
+use lithium\tests\mocks\data\source\mongo_db\MockResult;
+use lithium\tests\mocks\data\model\MockDocumentMultipleKey;
+
+/**
+ * DocumentSet tests
+ */
+class DocumentSetTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\model\MockDocumentPost';
+
+ protected $_preserved = array();
+
+ public function skip() {
+ $this->skipIf(!MongoDb::enabled(), 'MongoDb is not enabled');
+ $this->skipIf(!CouchDb::enabled(), 'CouchDb is not enabled');
+ }
+
+ public function setUp() {
+ if (empty($this->_preserved)) {
+ foreach (Connections::get() as $conn) {
+ $this->_preserved[$conn] = Connections::get($conn, array('config' => true));
+ }
+ }
+ Connections::reset();
+
+ Connections::add('mongo', array('type' => 'MongoDb', 'autoConnect' => false));
+ Connections::add('couch', array('type' => 'http', 'adapter' => 'CouchDb'));
+
+ MockDocumentPost::config(array('connection' => 'mongo'));
+ MockDocumentMultipleKey::config(array('connection' => 'couch'));
+ }
+
+ public function tearDown() {
+ foreach ($this->_preserved as $name => $config) {
+ Connections::add($name, $config);
+ }
+ }
+
+ public function testPopulateResourceClose() {
+ $resource = new MockResult();
+ $doc = new DocumentSet(array('model' => $this->_model, 'result' => $resource));
+ $model = $this->_model;
+
+ $result = $doc->rewind();
+ $this->assertTrue($result instanceof Document);
+ $this->assertTrue(is_object($result['_id']));
+
+ $expected = array('_id' => '4c8f86167675abfabdbf0300', 'title' => 'bar');
+ $this->assertEqual($expected, $result->data());
+
+ $expected = array('_id' => '5c8f86167675abfabdbf0301', 'title' => 'foo');
+ $this->assertEqual($expected, $doc->next()->data());
+
+ $expected = array('_id' => '6c8f86167675abfabdbf0302', 'title' => 'dib');
+ $result = $doc->next()->data();
+ $this->assertEqual($expected, $result);
+
+ $this->assertNull($doc->next());
+ }
+
+ public function testMappingToNewDocumentSet() {
+ $result = new MockResult();
+ $model = $this->_model;
+ $doc = new DocumentSet(compact('model', 'result'));
+
+ $mapped = $doc->map(function($data) { return $data; });
+ $this->assertEqual($doc->data(), $mapped->data());
+ $this->assertEqual($model, $doc->model());
+ $this->assertEqual($model, $mapped->model());
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/collection/DocumentTest.php b/libraries/lithium/tests/cases/data/collection/DocumentTest.php
deleted file mode 100644
index 97c50e3..0000000
--- a/libraries/lithium/tests/cases/data/collection/DocumentTest.php
+++ /dev/null
@@ -1,570 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\data\collection;
-
-use \stdClass;
-use \lithium\data\collection\Document;
-use \lithium\tests\mocks\data\model\MockDocumentPost;
-use \lithium\tests\mocks\data\model\MockDocumentSource;
-use \lithium\tests\mocks\data\model\MockDocumentMultipleKey;
-
-class DocumentTest extends \lithium\test\Unit {
-
- public function testFindAllAndIterate() {
- $document = MockDocumentPost::find('all');
-
- $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
- $result = $document->current();
- $this->assertEqual($expected, $result);
-
- $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
- $result = $document->next()->data();
- $this->assertEqual($expected, $result);
-
- $expected = array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three');
- $document->next();
- $result = $document->current()->data();
- $this->assertEqual($expected, $result);
-
- $result = $document->next();
- $this->assertTrue(empty($result));
-
- $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
- $result = $document->rewind()->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testFindOne() {
- $document = MockDocumentPost::find('first');
-
- $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
- $result = $document->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testGetFields() {
- $document = MockDocumentPost::find('first');
-
- $expected = 2;
- $result = $document->id;
- $this->assertEqual($expected, $result);
-
- $result = $document['id'];
- $this->assertEqual($expected, $result);
-
- $expected = 'Two';
- $result = $document->name;
- $this->assertEqual($expected, $result);
-
- $result = $document['name'];
- $this->assertEqual($expected, $result);
-
- $expected = 'Lorem ipsum two';
- $result = $document->content;
- $this->assertEqual($expected, $result);
-
- $result = $document['content'];
- $this->assertEqual($expected, $result);
- }
-
- public function testSetField() {
- $doc = new Document();
- $doc->id = 4;
- $doc->name = 'Four';
- $doc->content = 'Lorem ipsum four';
-
- $expected = array(
- 'id' => 4,
- 'name' => 'Four',
- 'content' => 'Lorem ipsum four'
- );
- $result = $doc->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testNoItems() {
- $doc = new Document(array('items' => array()));
- $result = $doc->id;
- $this->assertFalse($result);
- }
-
- public function testWithData() {
- $doc = new Document(array('data' => array(
- array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one'),
- array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two'),
- array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three')
- )));
-
- $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
- $result = $doc->current();
- $this->assertEqual($expected, $result);
-
- $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
- $result = $doc->next()->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testExplicitSet() {
- $doc = new Document();
- $doc->set(array('id' => 4));
- $doc->set(array('name' => 'Four'));
- $doc->set(array('content' => 'Lorem ipsum four'));
-
- $expected = array('id' => 4, 'name' => 'Four', 'content' => 'Lorem ipsum four');
- $result = $doc->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testSetMultiple() {
- $doc = new Document();
- $doc->set(array(
- array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one'),
- array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two'),
- array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three')
- ));
- $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
- $result = $doc->current();
- $this->assertEqual($expected, $result);
-
- $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
- $result = $doc->next()->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testSetMultipleNested() {
- $doc = new Document();
- $doc->id = 123;
- $doc->type = 'father';
- $doc->set(array('children' => array(
- array('id' => 124, 'type' => 'child', 'children' => null),
- array('id' => 125, 'type' => 'child', 'children' => null)
- )));
-
- $this->assertEqual('father', $doc->type);
-
- $this->assertTrue(is_object($doc->children), 'children is not an object');
-
- $this->assertTrue(
- is_a($doc->children,'\lithium\data\collection\Document'),
- 'Children is not of the type Document'
- );
- $this->skipIf(
- !is_a($doc->children,'\lithium\data\collection\Document'),
- 'Children is not of the type Document'
- );
-
- $expected = array('id' => 124, 'type' => 'child', 'children' => null);
- $result = $doc->children->current();
- $this->assertEqual($expected, $result);
-
- $expected = array('id' => 125, 'type' => 'child', 'children' => null);
- $result = $doc->children->next()->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testSetNested() {
- $doc = new Document();
- $doc->id = 123;
- $doc->name = 'father';
- $doc->set(array('child' => array('id' => 124, 'name' => 'child')));
-
- $this->assertEqual('father', $doc->name);
-
- $this->assertTrue(is_object($doc->child), 'children is not an object');
- $this->assertTrue(is_a($doc->child, '\lithium\data\collection\Document'),
- 'Child is not of the type Document'
- );
- $this->skipIf(
- !is_a($doc->child,'\lithium\data\collection\Document'),
- 'Child is not of the type Document'
- );
-
- $expected = 124;
- $result = $doc->child->id;
- $this->assertEqual($expected, $result);
-
- $expected = 'child';
- $result = $doc->child->name;
- $this->assertEqual($expected, $result);
- }
-
- public function testNestedSingle() {
- $doc = new Document();
-
- $doc->arr1 = array('something' => 'else');
- $doc->arr2 = array('some' => 'noses', 'have' => 'it');
-
- $this->assertTrue(is_a($doc->arr1, '\lithium\data\collection\Document'));
- $this->assertTrue(is_a($doc->arr2, '\lithium\data\collection\Document'));
- }
-
- public function testRewindNoData() {
- $doc = new Document();
-
- $expected = null;
- $result = $doc->rewind();
- $this->assertEqual($expected, $result);
- }
-
- public function testRewindData() {
- $doc = new Document(array('items' => array(
- array('id' => 1, 'name' => 'One'),
- array('id' => 2, 'name' => 'Two'),
- array('id' => 3, 'name' => 'Three')
- )));
-
- $expected = array('id' => 1, 'name' => 'One');
- $result = $doc->rewind()->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testUpdateWithSingleKey() {
- $doc = new Document(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDocumentPost',
- ));
- $expected = 'id';
- $result = MockDocumentPost::meta('key');
- $this->assertEqual($expected, $result);
-
- $doc->id = 3;
- $this->assertFalse($doc->exists());
-
- $doc->update(12);
- $this->assertTrue($doc->exists());
-
- $expected = 12;
- $result = $doc->id;
- $this->assertEqual($expected, $result);
- }
-
- public function testUpdateWithMultipleKeys() {
- $doc = new Document(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDocumentMultipleKey',
- ));
- $expected = array('id', 'rev');
- $result = MockDocumentMultipleKey::meta('key');
- $this->assertEqual($expected, $result);
-
- $doc->id = 3;
- $this->assertFalse($doc->exists());
-
- $doc->update(array(12, '1-2'));
- $this->assertTrue($doc->exists());
-
- $expected = 12;
- $result = $doc->id;
- $this->assertEqual($expected, $result);
-
- $expected = '1-2';
- $result = $doc->rev;
- $this->assertEqual($expected, $result);
-
- }
-
- public function testArrayValueNestedDocument() {
- $doc = new Document(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDocumentPost',
- 'items' => array(
- 'id' => 12, 'arr' => array('id' => 33, 'name' => 'stone'), 'name' => 'bird'
- )
- ));
-
- $expected = 12;
- $result = $doc->id;
- $this->assertEqual($expected, $result);
-
- $expected = 'bird';
- $result = $doc->name;
- $this->assertEqual($expected, $result);
-
- $this->assertTrue(is_object($doc->arr), 'arr is not an object');
- $this->assertTrue(
- is_a($doc->arr,'\lithium\data\collection\Document'),
- 'arr is not of the type Document'
- );
- $this->skipIf(
- !is_a($doc->arr,'\lithium\data\collection\Document'),
- 'arr is not of the type Document'
- );
-
- $expected = 33;
- $result = $doc->arr->id;
- $this->assertEqual($expected, $result);
-
- $expected = 'stone';
- $result = $doc->arr->name;
- $this->assertEqual($expected, $result);
- }
-
- public function testArrayValueGet() {
- $doc = new Document(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDocumentPost',
- 'items' => array('id' => 12, 'name' => 'Joe', 'sons' => array('Moe', 'Greg'))
- ));
-
- $expected = 12;
- $result = $doc->id;
- $this->assertEqual($expected, $result);
-
- $expected = 'Joe';
- $result = $doc->name;
- $this->assertEqual($expected, $result);
-
- $this->assertTrue(is_array($doc->sons), 'arr is not an array');
-
- $expected = array('Moe', 'Greg');
- $result = $doc->sons;
- $this->assertEqual($expected, $result);
- }
-
- public function testArrayValueSet() {
- $doc = new Document();
-
- $doc->id = 12;
- $doc->name = 'Joe';
- $doc->sons = array('Moe', 'Greg',12, 0.3);
- $doc->set(array('daughters' => array('Susan', 'Tinkerbell')));
-
- $expected = array(
- 'id' => 12,
- 'name' => 'Joe',
- 'sons' => array('Moe', 'Greg', 12, 0.3),
- 'daughters' => array('Susan', 'Tinkerbell')
- );
- $result = $doc->data();
- $this->assertEqual($expected, $result);
- }
-
- public function testCall() {
- $doc = new Document();
-
- $result = $doc->medicin();
- $this->assertNull($result);
-
- $doc = new Document(array('model' => 'lithium\tests\mocks\data\model\MockDocumentPost'));
-
- $expected = 'lithium';
- $result = $doc->medicin();
- $this->assertEqual($expected, $result);
-
- $result = $doc->ret();
- $this->assertNull($result);
-
- $expected = 'nose';
- $result = $doc->ret('nose');
- $this->assertEqual($expected, $result);
-
- $expected = 'job';
- $result = $doc->ret('nose','job');
- $this->assertEqual($expected, $result);
-
- }
-
- public function testPopulateResourceClose() {
- $resource = new MockDocumentSource();
- $resource->read();
- $doc = new Document(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDocumentPost',
- 'handle' => new MockDocumentSource(),
- 'result' => $resource
- ));
-
- $result = $doc->rewind();
- $this->assertTrue(is_a($result,'\lithium\data\collection\Document'));
-
- $expected = array('id' => 2, 'name' => 'Moe');
- $result = $doc->next()->data();
- $this->assertEqual($expected, $result);
-
- $expected = array('id' => 3, 'name' => 'Roe');
- $result = $doc->next()->data();
- $this->assertEqual($expected, $result);
-
- $result = $doc->next();
- $this->assertNull($result);
- }
-
- public function testEmptyValues() {
- $doc = new Document(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDocumentPost',
- 'data' => array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- 'parsed' => null,
- 'permanent' => false,
- )
- ));
-
- $expected = array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- 'parsed' => null,
- 'permanent' => false,
- );
- $result = $doc->data();
- $this->assertEqual($expected, $result);
- }
-
- /**
- * Tests that `Document`s with embedded objects are cast to arrays so as not to cause fatal
- * errors when traversing via array interfaces.
- *
- * @return void
- */
- public function testObjectIteration() {
- $doc = new Document(array('data' => array(
- (object) array('foo' => 'bar'),
- (object) array('bar' => 'foo')
- )));
- $result = $doc->first()->foo;
- $expected = 'bar';
- $this->assertEqual($expected, $result);
-
- $result = $doc->next()->bar;
- $expected = 'foo';
- $this->assertEqual($expected, $result);
-
- $doc = new Document(array('data' => (object) array(
- 'first' => array('foo' => 'bar'),
- 'second' => array('bar' => 'foo')
- )));
- $result = $doc->first->foo;
- }
-
- public function testBooleanValues() {
- $doc = new Document();
-
- $doc->tall = false;
- $doc->fat = true;
- $doc->set(array('hair' => true, 'fast' => false));
-
- $expected = array('hair', 'fast', 'tall', 'fat');
- $result = array_keys($doc->data());
- $this->assertEqual($expected, $result);
- }
-
- public function testComplexTypes() {
- $doc = new Document();
- $this->assertFalse($doc->invoke('isComplexType', array(null)));
- $this->assertFalse($doc->invoke('isComplexType', array('')));
- $this->assertFalse($doc->invoke('isComplexType', array(array())));
- $this->assertFalse($doc->invokeMethod('_isComplexType',array(new stdClass())));
- }
-
- public function testIsset() {
- $doc = new Document(array('data' => array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- )));
-
- $this->assertTrue(isset($doc->title));
- $this->assertTrue(isset($doc->content));
- $this->assertFalse(isset($doc->body));
- }
-
- public function testData() {
- $doc = new Document(array(
- 'data' => array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- 'parsed' => null,
- 'permanent' => false
- )
- ));
-
- $expected = array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- 'parsed' => null,
- 'permanent' => false
- );
- $result = $doc->data();
- $this->assertEqual($expected, $result);
-
- $expected = 'Post';
- $result = $doc->data('title');
- $this->assertEqual($expected, $result);
-
- $expected = false;
- $result = $doc->data('permanent');
- $this->assertEqual($expected, $result);
-
- $doc = new Document();
- $this->assertNull($doc->data('field'));
- }
-
- public function testUnset() {
- $doc = new Document(array(
- 'data' => array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- 'parsed' => null,
- 'permanent' => false
- )
- ));
-
- $expected = array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- 'parsed' => null,
- 'permanent' => false
- );
- $result = $doc->data();
- $this->assertEqual($expected, $result);
-
- unset($expected['title']);
- unset($doc->title);
- $result = $doc->data();
- $this->assertEqual($expected, $result);
-
- unset($expected['parsed']);
- unset($doc->parsed);
- $result = $doc->data();
- $this->assertEqual($expected, $result);
-
- unset($expected['permanent']);
- unset($doc->permanent);
- $result = $doc->data();
- $this->assertEqual($expected, $result);
-
- unset($doc->none);
- }
-
- public function testErrors() {
- $doc = new Document(array(
- 'data' => array(
- 'title' => 'Post',
- 'content' => 'Lorem Ipsum',
- 'parsed' => null,
- 'permanent' => false
- )
- ));
-
- $errors = array(
- 'title' => 'Too short',
- 'parsed' => 'Empty'
- );
- $doc->errors($errors);
-
- $expected = $errors;
- $result = $doc->errors();
- $this->assertEqual($expected, $result);
-
- $expected = 'Too short';
- $result = $doc->errors('title');
- $this->assertEqual($expected, $result);
-
- $doc->errors('title', 'Too generic');
- $expected = 'Too generic';
- $result = $doc->errors('title');
- $this->assertEqual($expected, $result);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/collection/RecordSetTest.php b/libraries/lithium/tests/cases/data/collection/RecordSetTest.php
index aca56c1..83b2b4d 100644
--- a/libraries/lithium/tests/cases/data/collection/RecordSetTest.php
+++ b/libraries/lithium/tests/cases/data/collection/RecordSetTest.php
@@ -2,23 +2,28 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data\collection;
-use \lithium\tests\mocks\data\collection\MockRecordSet;
-use \lithium\tests\mocks\data\source\database\adapter\MockAdapter;
-use \lithium\tests\mocks\data\MockPostObject;
-use \lithium\tests\mocks\data\MockModel;
-use \lithium\data\Connections;
-use \lithium\util\Collection;
+use lithium\data\collection\RecordSet;
+use lithium\tests\mocks\data\collection\MockRecordSet;
+use lithium\tests\mocks\data\source\database\adapter\MockAdapter;
+use lithium\tests\mocks\data\model\mock_database\MockResult;
+use lithium\tests\mocks\data\MockPostObject;
+use lithium\tests\mocks\data\MockModel;
+use lithium\data\Connections;
+use lithium\util\Collection;
/**
* RecordSet tests
*/
class RecordSetTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\MockModel';
+
/**
* RecordSet object to test
*
@@ -56,51 +61,33 @@ class RecordSetTest extends \lithium\test\Unit {
foreach($this->_records as $i => $record) {
$this->_objectRecords[$i] = new MockPostObject($record);
}
+ $result = new MockResult();
+ $result->records = array_merge(array(false), $this->_records);
+ $model = $this->_model;
- $connection = new MockAdapter(array(
- 'records' => $this->_records,
- 'columns' => array('Test' => array('id', 'data')),
- 'autoConnect' => false
- ));
-
- $this->_recordSet = new MockRecordSet(array(
- 'model' => 'lithium\tests\mocks\data\MockModel',
- 'handle' => &$connection,
- 'result' => true,
- 'exists' => true,
- ));
-
- $objectConnection = new MockAdapter(array(
- 'records' => $this->_objectRecords,
- 'columns' => array('MockPostObject' => array('id', 'data')),
- 'autoConnect' => false
- ));
+ $this->_recordSet = new MockRecordSet(compact('result', 'model') + array('exists' => true));
- $this->_objectRecordSet = new MockRecordSet(array(
- 'model' => 'lithium\tests\mocks\data\MockModel',
- 'handle' => &$objectConnection,
- 'result' => true,
- 'exists' => true,
+ $result = new MockResult();
+ $result->records = array_merge(array(false), $this->_records);
+ $this->_objectRecordSet = new MockRecordSet(compact('result', 'model') + array(
+ 'exists' => true
));
}
public function testInit() {
$recordSet = new MockRecordSet();
-
- $this->assertTrue(is_a($recordSet, '\lithium\data\collection\RecordSet'));
+ $this->assertTrue($recordSet instanceof RecordSet);
$recordSet = new MockRecordSet(array(
- 'model' => 'lithium\tests\mocks\data\MockModel',
- 'handle' => new MockAdapter(),
+ 'model' => $this->_model,
'result' => true,
'exists' => true,
));
- $this->assertEqual('lithium\tests\mocks\data\MockModel', $recordSet->get('_model'));
+ $this->assertEqual($this->_model, $recordSet->model());
$this->assertTrue($recordSet->get('_result'));
}
-
public function testOffsetExists() {
$this->assertFalse($this->_recordSet->offsetExists(0));
$this->assertTrue($this->_recordSet->offsetExists(1));
@@ -134,6 +121,9 @@ class RecordSetTest extends \lithium\test\Unit {
$expected = array('id' => 3, 'data' => 'data3');
$this->assertEqual($this->_records[2], $this->_recordSet[3]->to('array'));
+ $recordSet = new MockRecordSet();
+ $this->assertEqual(array(), $recordSet->data());
+
$this->expectException();
$this->_recordSet[5];
}
@@ -191,20 +181,22 @@ class RecordSetTest extends \lithium\test\Unit {
public function testOffsetSet() {
$this->_recordSet[5] = $expected = array('id' => 5, 'data' => 'data5');
- $items = $this->_recordSet->get('_items');
+ $items = $this->_recordSet->get('_data');
$this->assertEqual($expected, $items[0]->to('array'));
$this->_recordSet[] = $expected = array('id' => 6, 'data' => 'data6');
- $items = $this->_recordSet->get('_items');
+ $items = $this->_recordSet->get('_data');
$this->assertEqual($expected, $items[1]->to('array'));
- $this->_objectRecordSet[5] = $expected = new MockPostObject(array('id' => 5, 'data' => 'data5'));
- $items = $this->_recordSet->get('_items');
+ $this->_objectRecordSet[5] = $expected = new MockPostObject(array(
+ 'id' => 5, 'data' => 'data5'
+ ));
+ $items = $this->_recordSet->get('_data');
$this->assertEqual($expected->id, $items[0]->id);
$this->assertEqual($expected->data, $items[0]->data);
$this->_recordSet[] = $expected = new MockPostObject(array('id' => 6, 'data' => 'data6'));
- $items = $this->_recordSet->get('_items');
+ $items = $this->_recordSet->get('_data');
$this->assertEqual($expected->id, $items[1]->id);
$this->assertEqual($expected->data, $items[1]->data);
}
@@ -415,13 +407,13 @@ class RecordSetTest extends \lithium\test\Unit {
$result = $this->_recordSet->map($filter);
- $this->assertEqual($expected, $result->get('_items'));
+ $this->assertEqual($expected, $result->get('_data'));
$result = $this->_objectRecordSet->map($filter, array('collect' => false));
$this->assertEqual($expected, $result);
$result = $this->_objectRecordSet->map($filter);
- $this->assertEqual($expected, $result->get('_items'));
+ $this->assertEqual($expected, $result->get('_data'));
}
}
diff --git a/libraries/lithium/tests/cases/data/entity/DocumentTest.php b/libraries/lithium/tests/cases/data/entity/DocumentTest.php
new file mode 100644
index 0000000..9e3e8f4
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/entity/DocumentTest.php
@@ -0,0 +1,666 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\data\entity;
+
+use MongoId;
+use MongoDate;
+use lithium\data\Connections;
+use lithium\data\source\MongoDb;
+use lithium\data\source\http\adapter\CouchDb;
+use lithium\data\entity\Document;
+use lithium\data\collection\DocumentSet;
+use lithium\data\collection\DocumentArray;
+use lithium\tests\mocks\data\model\MockDocumentPost;
+use lithium\tests\mocks\data\model\MockDocumentSource;
+use lithium\tests\mocks\data\model\MockDocumentMultipleKey;
+
+class DocumentTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\model\MockDocumentPost';
+
+ protected $_preserved = array();
+
+ public function skip() {
+ $this->skipIf(!MongoDb::enabled(), 'MongoDb is not enabled');
+ $this->skipIf(!CouchDb::enabled(), 'CouchDb is not enabled');
+ }
+
+ public function setUp() {
+ if (empty($this->_preserved)) {
+ foreach (Connections::get() as $conn) {
+ $this->_preserved[$conn] = Connections::get($conn, array('config' => true));
+ }
+ }
+ Connections::reset();
+
+ Connections::add('mongo', array('type' => 'MongoDb', 'autoConnect' => false));
+ Connections::add('couch', array('type' => 'http', 'adapter' => 'CouchDb'));
+
+ MockDocumentPost::config(array('connection' => 'mongo'));
+ MockDocumentMultipleKey::config(array('connection' => 'couch'));
+ }
+
+ public function tearDown() {
+ foreach ($this->_preserved as $name => $config) {
+ Connections::add($name, $config);
+ }
+ }
+
+ public function testFindAllAndIterate() {
+ $set = MockDocumentPost::find('all');
+
+ $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
+ $result = $set->current()->data();
+ $this->assertEqual($expected, $result);
+
+ $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
+ $result = $set->next()->data();
+ $this->assertEqual($expected, $result);
+
+ $expected = array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three');
+ $set->next();
+ $result = $set->current()->data();
+ $this->assertEqual($expected, $result);
+
+ $result = $set->next();
+ $this->assertTrue(empty($result));
+
+ $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
+ $result = $set->rewind()->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testFindOne() {
+ $document = MockDocumentPost::find('first');
+
+ $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
+ $result = $document->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testGetFields() {
+ $document = MockDocumentPost::find('first');
+
+ $expected = 2;
+ $result = $document->id;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Two';
+ $result = $document->name;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Lorem ipsum two';
+ $result = $document->content;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSetField() {
+ $doc = new Document();
+ $doc->id = 4;
+ $doc->name = 'Four';
+ $doc->content = 'Lorem ipsum four';
+
+ $expected = array(
+ 'id' => 4,
+ 'name' => 'Four',
+ 'content' => 'Lorem ipsum four'
+ );
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testNestedKeyGetSet() {
+ $doc = new Document(array('model' => $this->_model, 'data' => array(
+ 'name' => 'Bob', 'location' => 'New York, NY', 'profile' => array(
+ 'occupation' => 'Developer', 'likes' => 'PHP', 'dislikes' => 'Java'
+ )
+ )));
+
+ $expected = array('occupation' => 'Developer', 'likes' => 'PHP', 'dislikes' => 'Java');
+ $this->assertEqual($expected, $doc->profile->data());
+ $this->assertEqual('Java', $doc->profile->dislikes);
+ $this->assertEqual('Java', $doc->{'profile.dislikes'});
+ $this->assertNull($doc->{'profile.'});
+ $this->assertNull($doc->{'profile.foo'});
+ $this->assertNull($doc->{'profile.foo.bar'});
+
+ $doc->{'profile.dislikes'} = 'Crystal Reports';
+ $this->assertEqual('Crystal Reports', $doc->profile->dislikes);
+
+ $doc->{'profile.foo.bar'} = 'baz';
+ $this->assertTrue($doc->profile->foo instanceof Document);
+ $this->assertEqual(array('bar' => 'baz'), $doc->profile->foo->data());
+
+ $post = new Document(array('model' => $this->_model, 'data' => array(
+ 'title' => 'Blog Post',
+ 'body' => 'Some post content.',
+ 'meta' => array('tags' => array('foo', 'bar', 'baz'))
+ )));
+ $this->assertEqual(array('foo', 'bar', 'baz'), $post->meta->tags->data());
+
+ $post->{'meta.tags'}[] = 'dib';
+ $this->assertEqual(array('foo', 'bar', 'baz', 'dib'), $post->meta->tags->data());
+ }
+
+ public function testNoItems() {
+ $doc = new Document(array('model' => $this->_model, 'data' => array()));
+ $result = $doc->id;
+ $this->assertFalse($result);
+ }
+
+ public function testWithData() {
+ $doc = new DocumentSet(array('model' => $this->_model, 'data' => array(
+ array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one'),
+ array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two'),
+ array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three')
+ )));
+
+ $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
+ $result = $doc->current()->data();
+ $this->assertEqual($expected, $result);
+
+ $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
+ $result = $doc->next()->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExplicitSet() {
+ $doc = new Document();
+ $doc->set(array('id' => 4));
+ $doc->set(array('name' => 'Four'));
+ $doc->set(array('content' => 'Lorem ipsum four'));
+
+ $expected = array('id' => 4, 'name' => 'Four', 'content' => 'Lorem ipsum four');
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSetMultiple() {
+ $doc = new DocumentSet(array('model' => $this->_model));
+ $doc->set(array(
+ array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one'),
+ array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two'),
+ array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three')
+ ));
+ $expected = array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one');
+ return;
+ $result = $doc->current()->data();
+ $this->assertEqual($expected, $result);
+
+ $expected = array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two');
+ $result = $doc->next()->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSetMultipleNested() {
+ $doc = new Document(array('model' => $this->_model));
+ $doc->id = 123;
+ $doc->type = 'father';
+ $doc->set(array('children' => array(
+ array('id' => 124, 'type' => 'child', 'children' => null),
+ array('id' => 125, 'type' => 'child', 'children' => null)
+ )));
+
+ $this->assertEqual('father', $doc->type);
+
+ $this->assertTrue(is_object($doc->children), 'children is not an object');
+
+ $this->assertTrue($doc->children instanceof DocumentArray);
+
+ $expected = array('id' => 124, 'type' => 'child', 'children' => null);
+ $result = $doc->children[0]->data();
+ $this->assertEqual($expected, $result);
+
+ $expected = array('id' => 125, 'type' => 'child', 'children' => null);
+ $result = $doc->children[1]->data();
+ // @todo Make $result = $doc->children->{1}->data(); work as well (and ...->{'1'}->...)
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSetNested() {
+ $doc = new Document(array('model' => $this->_model));
+ $doc->id = 123;
+ $doc->name = 'father';
+ $doc->set(array('child' => array('id' => 124, 'name' => 'child')));
+
+ $this->assertEqual('father', $doc->name);
+
+ $this->assertTrue(is_object($doc->child), 'children is not an object');
+ $this->assertTrue($doc->child instanceof Document, 'Child is not of the type Document');
+ $this->skipIf(!$doc->child instanceof Document, 'Child is not of the type Document');
+
+ $expected = 124;
+ $result = $doc->child->id;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'child';
+ $result = $doc->child->name;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testNestedSingle() {
+ $doc = new Document(array('model' => $this->_model));
+
+ $doc->arr1 = array('something' => 'else');
+ $doc->arr2 = array('some' => 'noses', 'have' => 'it');
+
+ $this->assertTrue($doc->arr1 instanceof Document);
+ $this->assertTrue($doc->arr2 instanceof Document);
+ }
+
+ public function testRewindNoData() {
+ $doc = new DocumentSet();
+ $result = $doc->rewind();
+ $this->assertNull($result);
+ }
+
+ public function testRewindData() {
+ $doc = new DocumentSet(array('model' => $this->_model, 'data' => array(
+ array('id' => 1, 'name' => 'One'),
+ array('id' => 2, 'name' => 'Two'),
+ array('id' => 3, 'name' => 'Three')
+ )));
+
+ $expected = array('id' => 1, 'name' => 'One');
+ $result = $doc->rewind()->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testUpdateWithSingleKey() {
+ $doc = new Document(array('model' => $this->_model));
+ $result = MockDocumentPost::meta('key');
+ $this->assertEqual('_id', $result);
+
+ $doc->_id = 3;
+ $this->assertFalse($doc->exists());
+
+ $doc->update(12);
+ $this->assertTrue($doc->exists());
+ $this->assertEqual(12, $doc->_id);
+ }
+
+ public function testUpdateWithMultipleKeys() {
+ $model = 'lithium\tests\mocks\data\model\MockDocumentMultipleKey';
+ $model::config(array('key' => array('id', 'rev'), 'foo' => true));
+ $doc = new Document(compact('model'));
+
+ $result = $model::meta('key');
+ $this->assertEqual(array('id', 'rev'), $result);
+
+ $doc->id = 3;
+ $this->assertFalse($doc->exists());
+
+ $doc->update(array(12, '1-2'));
+ $this->assertTrue($doc->exists());
+
+ $this->assertEqual(12, $doc->id);
+ $this->assertEqual('1-2', $doc->rev);
+ }
+
+ public function testArrayValueNestedDocument() {
+ $doc = new Document(array(
+ 'model' => 'lithium\tests\mocks\data\model\MockDocumentPost',
+ 'data' => array(
+ 'id' => 12, 'arr' => array('id' => 33, 'name' => 'stone'), 'name' => 'bird'
+ )
+ ));
+
+ $this->assertEqual(12, $doc->id);
+ $this->assertEqual('bird', $doc->name);
+
+ $this->assertTrue(is_object($doc->arr), 'arr is not an object');
+ $this->assertTrue($doc->arr instanceof Document, 'arr is not of the type Document');
+ $this->skipIf(!$doc->arr instanceof Document, 'arr is not of the type Document');
+
+ $this->assertEqual(33, $doc->arr->id);
+ $this->assertEqual('stone', $doc->arr->name);
+ }
+
+ public function testArrayValueGet() {
+ $doc = new Document(array(
+ 'model' => $this->_model,
+ 'data' => array('id' => 12, 'name' => 'Joe', 'sons' => array('Moe', 'Greg'))
+ ));
+
+ $this->assertEqual(12, $doc->id);
+ $this->assertEqual('Joe', $doc->name);
+
+ $this->assertTrue($doc->sons instanceof DocumentArray, 'arr is not an array');
+ $this->assertEqual(array('Moe', 'Greg'), $doc->sons->data());
+ }
+
+ public function testArrayValueSet() {
+ $doc = new Document(array('model' => $this->_model));
+
+ $doc->id = 12;
+ $doc->name = 'Joe';
+ $doc->sons = array('Moe', 'Greg', 12, 0.3);
+ $doc->set(array('daughters' => array('Susan', 'Tinkerbell')));
+
+ $expected = array(
+ 'id' => 12,
+ 'name' => 'Joe',
+ 'sons' => array('Moe', 'Greg', 12, 0.3),
+ 'daughters' => array('Susan', 'Tinkerbell')
+ );
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInvalidCall() {
+ $doc = new Document();
+
+ $this->expectException("No model bound or unhandled method call `medicin`.");
+ $result = $doc->medicin();
+ $this->assertNull($result);
+ }
+
+ public function testCall() {
+ $doc = new Document(array('model' => 'lithium\tests\mocks\data\model\MockDocumentPost'));
+
+ $expected = 'lithium';
+ $result = $doc->medicin();
+ $this->assertEqual($expected, $result);
+
+ $result = $doc->ret();
+ $this->assertNull($result);
+
+ $expected = 'nose';
+ $result = $doc->ret('nose');
+ $this->assertEqual($expected, $result);
+
+ $expected = 'job';
+ $result = $doc->ret('nose','job');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testEmptyValues() {
+ $doc = new Document(array(
+ 'model' => 'lithium\tests\mocks\data\model\MockDocumentPost',
+ 'data' => array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum',
+ 'parsed' => null,
+ 'permanent' => false,
+ )
+ ));
+
+ $expected = array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum',
+ 'parsed' => null,
+ 'permanent' => false,
+ );
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testBooleanValues() {
+ $doc = new Document(array('model' => $this->_model));
+
+ $doc->tall = false;
+ $doc->fat = true;
+ $doc->set(array('hair' => true, 'fast' => false));
+
+ $expected = array('tall', 'fat', 'hair', 'fast');
+ $this->assertEqual($expected, array_keys($doc->data()));
+ }
+
+ public function testIsset() {
+ $doc = new Document(array('data' => array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum'
+ )));
+
+ $this->assertTrue(isset($doc->title));
+ $this->assertTrue(isset($doc->content));
+ $this->assertFalse(isset($doc->body));
+ }
+
+ public function testData() {
+ $doc = new Document(array(
+ 'data' => array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum',
+ 'parsed' => null,
+ 'permanent' => false
+ )
+ ));
+
+ $expected = array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum',
+ 'parsed' => null,
+ 'permanent' => false
+ );
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Post';
+ $result = $doc->data('title');
+ $this->assertEqual($expected, $result);
+
+ $result = $doc->data('permanent');
+ $this->assertFalse($result);
+
+ $doc = new Document();
+ $this->assertNull($doc->data('field'));
+ }
+
+ public function testUnset() {
+ $doc = new Document(array(
+ 'data' => array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum',
+ 'parsed' => null,
+ 'permanent' => false
+ )
+ ));
+
+ $expected = array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum',
+ 'parsed' => null,
+ 'permanent' => false
+ );
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+
+ unset($expected['title']);
+ unset($doc->title);
+ $this->assertEqual($expected, $doc->data());
+
+ unset($expected['parsed']);
+ unset($doc->parsed);
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+
+ unset($expected['permanent']);
+ unset($doc->permanent);
+ $result = $doc->data();
+ $this->assertEqual($expected, $result);
+
+ unset($doc->none);
+ }
+
+ public function testErrors() {
+ $doc = new Document(array('data' => array(
+ 'title' => 'Post',
+ 'content' => 'Lorem Ipsum',
+ 'parsed' => null,
+ 'permanent' => false
+ )));
+
+ $errors = array('title' => 'Too short', 'parsed' => 'Empty');
+ $doc->errors($errors);
+
+ $expected = $errors;
+ $result = $doc->errors();
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Too short';
+ $result = $doc->errors('title');
+ $this->assertEqual($expected, $result);
+
+ $doc->errors('title', 'Too generic');
+ $expected = 'Too generic';
+ $result = $doc->errors('title');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testDocumentNesting() {
+ $model = $this->_model;
+ $data = array('top' => 'level', 'second' => array('level' => 'of data'));
+ $doc = new Document(compact('model', 'data'));
+
+ $this->assertTrue(isset($doc->top));
+ $this->assertTrue(isset($doc->second->level));
+ $this->assertTrue($doc->second instanceof Document);
+
+ $this->assertEqual('level', $doc->top);
+ $this->assertEqual('of data', $doc->second->level);
+ }
+
+ public function testPropertyIteration() {
+ $doc = new Document(array('data' => array('foo' => 'bar', 'baz' => 'dib')));
+ $keys = array(null, 'foo', 'baz');
+ $values = array(null, 'bar', 'dib');
+
+ foreach ($doc as $key => $value) {
+ $this->assertEqual(next($keys), $key);
+ $this->assertEqual(next($values), $value);
+ }
+ }
+
+ public function testExport() {
+ $data = array('foo' => 'bar', 'baz' => 'dib');
+ $doc = new Document(compact('data') + array('exists' => false));
+
+ $expected = array(
+ 'data' => array('foo' => 'bar', 'baz' => 'dib'),
+ 'update' => array(),
+ 'remove' => array(),
+ 'increment' => array(),
+ 'key' => '',
+ 'exists' => false
+ );
+ $this->assertEqual($expected, $doc->export());
+ }
+
+ /**
+ * Tests that a modified `Document` exports the proper fields in a newly-appended nested
+ * `Document`.
+ *
+ * @return void
+ */
+ public function testModifiedExport() {
+ $model = $this->_model;
+ $data = array('foo' => 'bar', 'baz' => 'dib');
+ $doc = new Document(compact('model', 'data') + array('exists' => false));
+
+ $doc->nested = array('more' => 'data');
+ $newData = $doc->export();
+
+ $expected = array('foo' => 'bar', 'baz' => 'dib', 'nested.more' => 'data');
+ $this->assertFalse($newData['exists']);
+ $this->assertEqual(array('foo' => 'bar', 'baz' => 'dib'), $newData['data']);
+ $this->assertEqual(1, count($newData['update']));
+ $this->assertTrue($newData['update']['nested'] instanceof Document);
+
+ $result = $newData['update']['nested']->export();
+ $this->assertFalse($result['exists']);
+ $this->assertEqual(array('more' => 'data'), $result['data']);
+ $this->assertFalse($result['update']);
+ $this->assertEqual('nested', $result['key']);
+
+ $doc = new Document(compact('model') + array('exists' => true, 'data' => array(
+ 'foo' => 'bar', 'baz' => 'dib'
+ )));
+
+ $result = $doc->export();
+ $this->assertFalse($result['update']);
+
+ $doc->nested = array('more' => 'data');
+ $this->assertEqual('data', $doc->nested->more);
+
+ $modified = $doc->export();
+ $this->assertTrue($modified['exists']);
+ $this->assertEqual(array('foo' => 'bar', 'baz' => 'dib'), $modified['data']);
+ $this->assertEqual(array('nested'), array_keys($modified['update']));
+ $this->assertNull($modified['key']);
+
+ $nested = $modified['update']['nested']->export();
+ $this->assertFalse($nested['exists']);
+ $this->assertEqual(array('more' => 'data'), $nested['data']);
+ $this->assertEqual('nested', $nested['key']);
+
+ $doc->update();
+ $result = $doc->export();
+ $this->assertFalse($result['update']);
+
+ $doc->more = 'cowbell';
+ $doc->nested->evenMore = 'cowbell';
+ $modified = $doc->export();
+
+ $expected = array('more' => 'cowbell');
+ $this->assertEqual($expected, $modified['update']);
+ $this->assertEqual(array('nested', 'foo', 'baz'), array_keys($modified['data']));
+ $this->assertEqual('bar', $modified['data']['foo']);
+ $this->assertEqual('dib', $modified['data']['baz']);
+
+ $nested = $modified['data']['nested']->export();
+ $this->assertTrue($nested['exists']);
+ $this->assertEqual(array('more' => 'data'), $nested['data']);
+ $this->assertEqual(array('evenMore' => 'cowbell'), $nested['update']);
+ $this->assertEqual('nested', $nested['key']);
+
+ $doc->update();
+ $doc->nested->evenMore = 'foo!';
+ $modified = $doc->export();
+ $this->assertFalse($modified['update']);
+
+ $nested = $modified['data']['nested']->export();
+ $this->assertEqual(array('evenMore' => 'foo!'), $nested['update']);
+ }
+
+ public function testArrayConversion() {
+ $doc = new Document(array('data' => array(
+ 'id' => new MongoId(),
+ 'date' => new MongoDate()
+ )));
+ $result = $doc->data();
+ $this->assertPattern('/^[a-f0-9]{24}$/', $result['id']);
+ $this->assertEqual(time(), $result['date']);
+ }
+
+ public function testInitializationWithNestedFields() {
+ $doc = new Document(array('model' => $this->_model, 'data' => array(
+ 'simple' => 'value',
+ 'nested.foo' => 'first',
+ 'nested.bar' => 'second',
+ 'really.nested.key' => 'value'
+ )));
+ $this->assertEqual('value', $doc->simple);
+ $this->assertEqual('first', $doc->nested->foo);
+ $this->assertEqual('second', $doc->nested->bar);
+ $this->assertEqual('value', $doc->really->nested->key);
+ $this->assertEqual(array('simple', 'nested', 'really'), array_keys($doc->data()));
+ }
+
+ public function testIdGetDoesNotSet() {
+ $document = MockDocumentPost::create();
+ $message = 'The `_id` key should not be set.';
+ $this->assertFalse(array_key_exists('_id', $document->data()), $message);
+
+ $document->_id == "";
+ $this->assertFalse(array_key_exists('_id', $document->data()), $message);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/entity/RecordTest.php b/libraries/lithium/tests/cases/data/entity/RecordTest.php
new file mode 100644
index 0000000..a3fba17
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/entity/RecordTest.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\data\entity;
+
+use lithium\data\Connections;
+use lithium\data\entity\Record;
+
+class RecordTest extends \lithium\test\Unit {
+
+ protected $_configs = array();
+
+ public function setUp() {
+ $this->_configs = Connections::config();
+
+ Connections::config(array('mock-source' => array(
+ 'type' => '\lithium\tests\mocks\data\MockSource'
+ )));
+ $model = 'lithium\tests\mocks\data\MockPost';
+ $model::config(array('connection' => 'mock-source', 'key' => 'id'));
+ $this->record = new Record(compact('model'));
+ }
+
+ public function tearDown() {
+ Connections::reset();
+ Connections::config($this->_configs);
+ }
+
+ /**
+ * Tests that a record's fields are accessible as object properties.
+ *
+ * @return void
+ */
+ public function testDataPropertyAccess() {
+ $data = array(
+ 'title' => 'Test record',
+ 'body' => 'Some test record data'
+ );
+
+ $this->record = new Record(compact('data'));
+
+ $expected = 'Test record';
+ $result = $this->record->title;
+ $this->assertEqual($expected, $result);
+ $this->assertTrue(isset($this->record->title));
+
+ $expected = 'Some test record data';
+ $result = $this->record->body;
+ $this->assertEqual($expected, $result);
+ $this->assertTrue(isset($this->record->body));
+
+ $this->assertNull($this->record->foo);
+ $this->assertFalse(isset($this->record->foo));
+ }
+
+ /**
+ * Tests that a record can be exported to a given series of formats.
+ *
+ * @return void
+ */
+ public function testRecordFormatExport() {
+ $data = array('foo' => 'bar');
+ $this->record = new Record(compact('data'));
+
+ $result = $this->record->to('array');
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->record->to('foo');
+ $this->assertEqual($this->record, $result);
+ }
+
+ public function testErrorsPropertyAccess() {
+ $errors = array(
+ 'title' => 'please enter a title',
+ 'email' => array('email is empty', 'email is not valid')
+ );
+
+ $record = new Record();
+ $result = $record->errors($errors);
+ $this->assertEqual($errors, $result);
+
+ $result = $record->errors();
+ $this->assertEqual($errors, $result);
+
+ $expected = 'please enter a title';
+ $result = $record->errors('title');
+ $this->assertEqual($expected, $result);
+
+ $expected = array('email is empty', 'email is not valid');
+ $result = $record->errors('email');
+ $this->assertEqual($expected, $result);
+
+ $result = $record->errors('not_a_field');
+ $this->assertNull($result);
+
+ $result = $record->errors('not_a_field', 'badness');
+ $this->assertEqual('badness', $result);
+ }
+
+ /**
+ * Test the ability to set multiple field's values, and that they can be read back.
+ */
+ public function testSetData() {
+ $this->assertFalse($this->record->data());
+ $expected = array('id' => 1, 'name' => 'Joe Bloggs', 'address' => 'The Park');
+ $this->record->set($expected);
+ $this->assertEqual($expected, $this->record->data());
+ $this->assertEqual($expected, $this->record->to('array'));
+ $this->assertEqual($expected['name'], $this->record->data('name'));
+ }
+
+ public function testRecordExists() {
+ $this->assertFalse($this->record->exists());
+ $this->record->update(313);
+ $this->assertIdentical(313, $this->record->id);
+ $this->assertTrue($this->record->exists());
+
+ $this->record = new Record(array('exists' => true));
+ $this->assertTrue($this->record->exists());
+ }
+
+ public function testMethodDispatch() {
+ $result = $this->record->save(array('title' => 'foo'));
+ $this->assertEqual('create', $result['query']->type());
+ $this->assertEqual(array('title' => 'foo'), $result['query']->data());
+
+ $this->expectException("No model bound or unhandled method call `invalid`.");
+ $this->assertNull($this->record->invalid());
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/model/QueryTest.php b/libraries/lithium/tests/cases/data/model/QueryTest.php
index 87decba..1df9981 100644
--- a/libraries/lithium/tests/cases/data/model/QueryTest.php
+++ b/libraries/lithium/tests/cases/data/model/QueryTest.php
@@ -2,35 +2,54 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data\model;
-use \lithium\data\model\Record;
-use \lithium\data\model\Query;
-use \lithium\tests\mocks\data\MockPostObject;
-use \lithium\tests\mocks\data\model\MockDatabase;
-use \lithium\tests\mocks\data\model\MockQueryPost;
-use \lithium\tests\mocks\data\model\MockQueryComment;
+use lithium\data\Connections;
+use lithium\data\model\Query;
+use lithium\data\entity\Record;
+use lithium\tests\mocks\data\MockPostObject;
+use lithium\tests\mocks\data\model\MockDatabase;
+use lithium\tests\mocks\data\model\MockQueryPost;
+use lithium\tests\mocks\data\model\MockQueryComment;
class QueryTest extends \lithium\test\Unit {
+ protected $_model = 'lithium\tests\mocks\data\model\MockQueryPost';
+
+ protected $_configs = array();
+
protected $_queryArr = array(
- 'model' => '\lithium\tests\mocks\data\model\MockQueryPost',
+ 'model' => 'lithium\tests\mocks\data\model\MockQueryPost',
'type' => 'read',
'order' => 'created DESC',
'limit' => 10,
'page' => 1,
- 'fields' => array('id','author_id','title'),
+ 'fields' => array('id', 'author_id', 'title'),
'conditions' => array('author_id' => 12),
'comment' => 'Find all posts by author 12'
);
public function setUp() {
- MockQueryPost::__init();
- MockQueryComment::__init();
+ $this->db = new MockDatabase();
+ $this->_configs = Connections::config();
+
+ Connections::reset();
+ Connections::config(array('mock-database-connection' => array(
+ 'object' => &$this->db,
+ 'adapter' => 'MockDatabase'
+ )));
+
+ MockQueryPost::config();
+ MockQueryComment::config();
+ }
+
+ public function tearDown() {
+ Connections::reset();
+ Connections::config($this->_configs);
}
/**
@@ -48,14 +67,11 @@ class QueryTest extends \lithium\test\Unit {
public function testModel() {
$query = new Query($this->_queryArr);
+ $this->assertEqual($this->_model, $query->model());
- $expected = '\lithium\tests\mocks\data\model\MockQueryPost';
- $result = $query->model();
- $this->assertEqual($expected, $result);
+ $query->model('lithium\tests\mocks\data\model\MockQueryComment');
- $query->model('\lithium\tests\mocks\data\model\MockQueryComment');
-
- $expected = '\lithium\tests\mocks\data\model\MockQueryComment';
+ $expected = 'lithium\tests\mocks\data\model\MockQueryComment';
$result = $query->model();
$this->assertEqual($expected, $result);
}
@@ -132,15 +148,14 @@ class QueryTest extends \lithium\test\Unit {
public function testRecord() {
$query = new Query($this->_queryArr);
- $result = $query->record();
+ $result = $query->entity();
$this->assertNull($result);
$record = (object) array('id' => 12);
$record->title = 'Lorem Ipsum';
- $query->record($record);
-
- $query_record = $query->record();
+ $query->entity($record);
+ $query_record = $query->entity();
$expected = 12;
$result = $query_record->id;
@@ -150,7 +165,7 @@ class QueryTest extends \lithium\test\Unit {
$result = $query_record->title;
$this->assertEqual($expected, $result);
- $this->assertTrue($record == $query->record());
+ $this->assertTrue($record == $query->entity());
}
public function testComment() {
@@ -178,7 +193,7 @@ class QueryTest extends \lithium\test\Unit {
$record->id = 12;
$record->title = 'Lorem Ipsum';
- $query->record($record);
+ $query->entity($record);
$expected = array('id' => 12, 'title' => 'Lorem Ipsum');
$result = $query->data();
@@ -206,11 +221,10 @@ class QueryTest extends \lithium\test\Unit {
}
public function testConditionFromRecord() {
- $r = new Record();
- $r->id = 12;
- $query = new Query(array(
- 'model' => '\lithium\tests\mocks\data\model\MockQueryPost',
- 'record' => $r
+ $entity = new Record();
+ $entity->id = 12;
+ $query = new Query(compact('entity') + array(
+ 'model' => $this->_model,
));
$expected = array('id' => 12);
@@ -219,7 +233,7 @@ class QueryTest extends \lithium\test\Unit {
}
public function testExtra() {
- $object = new MockPostObject(array('id'=>1, 'data'=>'test'));
+ $object = new MockPostObject(array('id' => 1, 'data' => 'test'));
$query = new Query(array(
'conditions' => 'foo', 'extra' => 'value', 'extraObject' => $object
));
@@ -230,6 +244,7 @@ class QueryTest extends \lithium\test\Unit {
}
public function testExport() {
+ MockQueryPost::meta('source', 'foo');
$query = new Query($this->_queryArr);
$ds = new MockDatabase();
$export = $query->export($ds);
@@ -238,18 +253,24 @@ class QueryTest extends \lithium\test\Unit {
$this->skipIf(!is_array($export), 'Query::export() does not return an array');
$expected = array(
+ 'alias',
+ 'calculate',
+ 'comment',
'conditions',
+ 'data',
'fields',
- 'order',
+ 'group',
+ 'joins',
'limit',
- 'table',
- 'comment',
+ 'map',
'model',
+ 'name',
+ 'offset',
+ 'order',
'page',
- 'group',
- 'joins',
- 'calculate',
- 'offset'
+ 'source',
+ 'type',
+ 'whitelist'
);
$result = array_keys($export);
@@ -261,8 +282,161 @@ class QueryTest extends \lithium\test\Unit {
$result = $export['fields'];
$this->assertEqual($expected, $result);
- $expected = MockQueryPost::meta('source');
- $result = $export['table'];
+ $result = $export['source'];
+ $this->assertEqual("{foo}", $result);
+ }
+
+ public function testRestrictedKeyExport() {
+ $options = array(
+ 'type' => 'update',
+ 'data' => array('title' => 'Bar'),
+ 'conditions' => array('title' => 'Foo'),
+ 'model' => $this->_model,
+ );
+ $query = new Query($options);
+
+ $result = $query->export(Connections::get('mock-database-connection'), array(
+ 'keys' => array('data', 'conditions')
+ ));
+ $expected = array(
+ 'type' => 'update',
+ 'data' => array('title' => 'Bar'),
+ 'conditions' => "WHERE {title} = 'Foo'",
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testPagination() {
+ $query = new Query(array('limit' => 5, 'page' => 1));
+ $this->assertEqual(0, $query->offset());
+
+ $query = new Query(array('limit' => 5, 'page' => 2));
+ $this->assertEqual(5, $query->offset());
+
+ $query->page(1);
+ $this->assertEqual(0, $query->offset());
+ }
+
+ public function testJoin() {
+ $query = new Query(array('joins' => array(array('foo' => 'bar'))));
+ $query->join(array('bar' => 'baz'));
+ $expected = array(array('foo' => 'bar'), array('bar' => 'baz'));
+ $joins = $query->join();
+
+ $this->assertEqual('bar', $joins[0]->foo());
+ $this->assertNull($joins[0]->bar());
+
+ $this->assertEqual('baz', $joins[1]->bar());
+ $this->assertNull($joins[1]->foo());
+
+ $query->join('zim', array('dib' => 'gir'));
+ $this->assertEqual(3, count($query->join()));
+
+ $expected = array(
+ array('foo' => 'bar'),
+ array('bar' => 'baz'),
+ 'zim' => array('dib' => 'gir')
+ );
+ $this->assertEqual(3, count($query->join()));
+ $this->assertEqual('gir', $query->join('zim')->dib());
+ }
+
+ /**
+ * Tests that assigning a whitelist to a query properly restricts the list of data fields that
+ * the query exposes.
+ *
+ * @return void
+ */
+ public function testWhitelisting() {
+ $data = array('foo' => 1, 'bar' => 2, 'baz' => 3);
+ $query = new Query(compact('data'));
+ $this->assertEqual($data, $query->data());
+
+ $query = new Query(compact('data') + array('whitelist' => array('foo', 'bar')));
+ $this->assertEqual(array('foo' => 1, 'bar' => 2), $query->data());
+ }
+
+ /**
+ * Tests basic property accessors and mutators.
+ *
+ * @return void
+ */
+ public function testBasicAssignments() {
+ $query = new Query();
+ $group = array('key' => 'hits', 'reduce' => 'function() {}');
+ $calculate = 'count';
+
+ $this->assertNull($query->group());
+ $query->group($group);
+ $this->assertEqual($group, $query->group());
+
+ $this->assertNull($query->calculate());
+ $query->calculate($calculate);
+ $this->assertEqual($calculate, $query->calculate());
+
+ $query = new Query(compact('calculate', 'group'));
+ $this->assertEqual($group, $query->group());
+ $this->assertEqual($calculate, $query->calculate());
+ }
+
+ public function testInstantiationWithConditionsAndData() {
+ $options = array(
+ 'type' => 'update',
+ 'data' => array('title' => '..'),
+ 'conditions' => array('title' => 'FML'),
+ 'model' => 'lithium\tests\mocks\data\model\MockQueryPost'
+ );
+ $query = new Query($options);
+ $result = $query->export(Connections::get('mock-database-connection'));
+
+ $this->assertEqual(array('title' => '..'), $result['data']);
+ $this->assertEqual("WHERE {title} = 'FML'", $result['conditions']);
+ }
+
+ public function testEntityConditions() {
+ $entity = new Record(array('model' => $this->_model, 'exists' => true));
+ $entity->id = 13;
+ $query = new Query(compact('entity'));
+ $this->assertEqual(array('id' => 13), $query->conditions());
+ }
+
+ public function testAutomaticAliasing() {
+ $query = new Query(array('model' => $this->_model));
+ $this->assertEqual('MockQueryPost', $query->alias());
+ }
+
+ public function testFluentInterface() {
+ $query = new Query();
+ $conditions = array('foo' => 'bar');
+ $fields = array('foo', 'bar', 'baz', 'created');
+ $order = array('created' => 'ASC');
+
+ $result = $query->conditions($conditions)->fields($fields)->order($order);
+ $this->assertEqual($result, $query);
+ $this->assertEqual($conditions, $query->conditions());
+ $this->assertEqual($fields, $query->fields());
+ $this->assertEqual($order, $query->order());
+ }
+
+ public function testRenderArrayJoin() {
+ $model = 'lithium\tests\mocks\data\model\MockQueryComment';
+
+ $query = new Query(compact('model') + array(
+ 'type' => 'read',
+ 'source' => 'comments',
+ 'alias' => 'Comment',
+ 'conditions' => array('Comment.id' => 1),
+ 'joins' => array(array(
+ 'type' => 'INNER',
+ 'source' => 'posts',
+ 'alias' => 'Post',
+ 'constraint' => array('Comment.post_id' => 'Post.id')
+ ))
+ ));
+
+ $expected = "SELECT * FROM AS {Comment} INNER JOIN {posts} AS {Post} ON ";
+ $expected .= "{Comment}.{post_id} = {Post}.{id} WHERE Comment.id = 1;";
+ $result = Connections::get('mock-database-connection')->renderCommand($query);
$this->assertEqual($expected, $result);
}
}
diff --git a/libraries/lithium/tests/cases/data/model/RecordTest.php b/libraries/lithium/tests/cases/data/model/RecordTest.php
deleted file mode 100644
index 43bad91..0000000
--- a/libraries/lithium/tests/cases/data/model/RecordTest.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\data\model;
-
-use \lithium\data\model\Record;
-
-class RecordTest extends \lithium\test\Unit {
-
- public function setUp() {
- $this->record = new Record();
- }
-
- /**
- * Tests that a record's fields are accessible as object properties.
- *
- * @return void
- */
- public function testDataPropertyAccess() {
- $data = array(
- 'title' => 'Test record',
- 'body' => 'Some test record data'
- );
-
- $this->record = new Record(compact('data'));
-
- $expected = 'Test record';
- $result = $this->record->title;
- $this->assertEqual($expected, $result);
- $this->assertTrue(isset($this->record->title));
-
- $expected = 'Some test record data';
- $result = $this->record->body;
- $this->assertEqual($expected, $result);
- $this->assertTrue(isset($this->record->body));
-
- $this->assertNull($this->record->foo);
- $this->assertFalse(isset($this->record->foo));
- }
-
- /**
- * Tests that a record can be exported to a given series of formats.
- *
- * @return void
- */
- public function testRecordFormatExport() {
- $data = array('foo' => 'bar');
- $this->record = new Record(compact('data'));
-
- $result = $this->record->to('array');
- $expected = $data;
- $this->assertEqual($expected, $result);
-
- $result = $this->record->to('foo');
- $this->assertEqual($this->record, $result);
- }
-
- public function testErrorsPropertyAccess() {
- $errors = array(
- 'title' => 'please enter a title',
- 'email' => array('email is empty', 'email is not valid')
- );
-
- $record = new Record();
- $result = $record->errors($errors);
- $this->assertEqual($errors, $result);
-
- $result = $record->errors();
- $this->assertEqual($errors, $result);
-
- $expected = 'please enter a title';
- $result = $record->errors('title');
- $this->assertEqual($expected, $result);
-
- $expected = array('email is empty', 'email is not valid');
- $result = $record->errors('email');
- $this->assertEqual($expected, $result);
-
- $result = $record->errors('not_a_field');
- $this->assertNull($result);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/source/DatabaseTest.php b/libraries/lithium/tests/cases/data/source/DatabaseTest.php
index 8a28414..e189f62 100644
--- a/libraries/lithium/tests/cases/data/source/DatabaseTest.php
+++ b/libraries/lithium/tests/cases/data/source/DatabaseTest.php
@@ -2,27 +2,46 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data\source;
-use \lithium\data\model\Query;
-use \lithium\data\model\Record;
-use \lithium\data\source\Database;
-use \lithium\tests\mocks\data\model\MockDatabase;
-use \lithium\tests\mocks\data\model\MockDatabasePost;
-use \lithium\tests\mocks\data\model\MockDatabaseComment;
+use lithium\data\Connections;
+use lithium\data\model\Query;
+use lithium\data\entity\Record;
+use lithium\data\source\Database;
+use lithium\data\collection\RecordSet;
+use lithium\tests\mocks\data\model\MockDatabase;
+use lithium\tests\mocks\data\model\MockDatabasePost;
+use lithium\tests\mocks\data\model\MockDatabaseComment;
class DatabaseTest extends \lithium\test\Unit {
public $db = null;
+ protected $_configs = array();
+
+ protected $_model = 'lithium\tests\mocks\data\model\MockDatabasePost';
+
public function setUp() {
$this->db = new MockDatabase();
- MockDatabasePost::__init();
- MockDatabaseComment::__init();
+ $this->_configs = Connections::config();
+
+ Connections::reset();
+ Connections::config(array('mock-database-connection' => array(
+ 'object' => &$this->db,
+ 'adapter' => 'MockDatabase'
+ )));
+
+ MockDatabasePost::config();
+ MockDatabaseComment::config();
+ }
+
+ public function tearDown() {
+ Connections::reset();
+ Connections::config($this->_configs);
}
public function testDefaultConfig() {
@@ -55,81 +74,57 @@ class DatabaseTest extends \lithium\test\Unit {
}
public function testName() {
- $expected = "name";
- $result = $this->db->name($expected);
- $this->assertEqual($expected, $result);
+ $result = $this->db->name("name");
+ $this->assertEqual("{name}", $result);
+
+ $result = $this->db->name("Model.name");
+ $this->assertEqual("{Model}.{name}", $result);
}
public function testValueWithSchema() {
- $expected = 'NULL';
$result = $this->db->value(null);
- $this->assertTrue(is_string($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical('NULL', $result);
- $expected = "'string'";
$result = $this->db->value('string', array('type' => 'string'));
- $this->assertTrue(is_string($result));
- $this->assertEqual($expected, $result);
+ $this->assertEqual("'string'", $result);
- $expected = true;
$result = $this->db->value('true', array('type' => 'boolean'));
- $this->assertTrue(is_bool($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical(1, $result);
- $expected = 1;
$result = $this->db->value('1', array('type' => 'integer'));
- $this->assertTrue(is_int($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical(1, $result);
- $expected = 1.1;
$result = $this->db->value('1.1', array('type' => 'float'));
- $this->assertTrue(is_float($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical(1.1, $result);
}
public function testValueByIntrospect() {
- $expected = "'string'";
$result = $this->db->value("string");
- $this->assertTrue(is_string($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical("'string'", $result);
- $expected = true;
$result = $this->db->value(true);
- $this->assertTrue(is_bool($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical(1, $result);
- $expected = 1;
$result = $this->db->value('1');
- $this->assertTrue(is_int($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical(1, $result);
- $expected = 1.1;
$result = $this->db->value('1.1');
- $this->assertTrue(is_float($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical(1.1, $result);
}
public function testSchema() {
- $expected = array(
- 'lithium\tests\mocks\data\model\MockDatabasePost' => array('id', 'title', 'created')
- );
- $result = $this->db->schema(new Query(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost'
- )));
+ $expected = array($this->_model => array(
+ 'id', 'author_id', 'title', 'created'
+ ));
+ $result = $this->db->schema(new Query(array('model' => $this->_model)));
$this->assertEqual($expected, $result);
- $query = new Query(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost',
- 'fields' => '*'
- ));
- $expected = array(
- 'lithium\tests\mocks\data\model\MockDatabasePost' => array('id', 'title', 'created')
- );
+ $query = new Query(array('model' => $this->_model, 'fields' => '*'));
$result = $this->db->schema($query);
$this->assertEqual($expected, $result);
$query = new Query(array(
- 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => $this->_model,
'fields' => array('MockDatabaseComment')
));
$expected = array(
@@ -141,137 +136,143 @@ class DatabaseTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testSchemaFromManualFieldList() {
+ $fields = array('id', 'name', 'created');
+ $result = $this->db->schema(new Query(compact('fields')));
+ $this->assertEqual(array($fields), $result);
+ }
+
public function testSimpleQueryRender() {
$result = $this->db->renderCommand(new Query(array(
'type' => 'read',
- 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => $this->_model,
'fields' => array('id', 'title', 'created')
)));
- $expected = 'SELECT id, title, created From mock_database_posts;';
+ $fields = 'id, title, created';
+ $table = '{mock_database_posts} AS {MockDatabasePost}';
+ $expected = "SELECT id, title, created FROM {$table};";
$this->assertEqual($expected, $result);
$result = $this->db->renderCommand(new Query(array(
'type' => 'read',
- 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => $this->_model,
'fields' => array('id', 'title', 'created'),
'limit' => 1
)));
- $expected = 'SELECT id, title, created From mock_database_posts LIMIT 1;';
+ $expected = 'SELECT id, title, created FROM {mock_database_posts} AS {MockDatabasePost} ';
+ $expected .= 'LIMIT 1;';
$this->assertEqual($expected, $result);
$result = $this->db->renderCommand(new Query(array(
'type' => 'read',
- 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => $this->_model,
'fields' => array('id', 'title', 'created'),
'limit' => 1,
'conditions' => 'Post.id = 2'
)));
- $expected = 'SELECT id, title, created From mock_database_posts WHERE Post.id = 2';
- $expected .= ' LIMIT 1;';
+ $expected = 'SELECT id, title, created FROM {mock_database_posts} AS {MockDatabasePost} ';
+ $expected .= 'WHERE Post.id = 2 LIMIT 1;';
$this->assertEqual($expected, $result);
}
public function testNestedQueryConditions() {
$query = new Query(array(
'type' => 'read',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => $this->_model,
'fields' => array('MockDatabasePost.title', 'MockDatabasePost.body'),
'conditions' => array('Post.id' => new Query(array(
'type' => 'read',
'fields' => array('post_id'),
- 'model' => '\lithium\tests\mocks\data\model\MockDatabaseTagging',
+ 'model' => 'lithium\tests\mocks\data\model\MockDatabaseTagging',
'conditions' => array('MockDatabaseTag.tag' => array('foo', 'bar', 'baz')),
)))
));
$result = $this->db->renderCommand($query);
- $expected = "SELECT MockDatabasePost.title, MockDatabasePost.body From mock_database_posts";
- $expected .= " WHERE Post.id IN (SELECT post_id From mock_database_taggings WHERE ";
- $expected .= "MockDatabaseTag.tag IN ('foo', 'bar', 'baz'));";
+ $expected = "SELECT MockDatabasePost.title, MockDatabasePost.body FROM";
+ $expected .= " {mock_database_posts} AS {MockDatabasePost} WHERE Post.id IN";
+ $expected .= " (SELECT post_id FROM {mock_database_taggings} AS {MockDatabaseTagging} ";
+ $expected .= "WHERE MockDatabaseTag.tag IN ('foo', 'bar', 'baz'));";
$this->assertEqual($expected, $result);
}
public function testJoin() {
$query = new Query(array(
'type' => 'read',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => $this->_model,
'fields' => array('MockDatabasePost.title', 'MockDatabasePost.body'),
'conditions' => array('MockDatabaseTag.tag' => array('foo', 'bar', 'baz')),
'joins' => array(new Query(array(
- 'type' => 'read',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabaseTag',
+ 'model' => 'lithium\tests\mocks\data\model\MockDatabaseTag',
'constraint' => 'MockDatabaseTagging.tag_id = MockDatabaseTag.id'
)))
));
$result = $this->db->renderCommand($query);
- $expected = "SELECT MockDatabasePost.title, MockDatabasePost.body From mock_database_posts";
- $expected .= " JOIN mock_database_tags ON MockDatabaseTagging.tag_id = MockDatabaseTag.id";
+ $expected = "SELECT MockDatabasePost.title, MockDatabasePost.body FROM";
+ $expected .= " {mock_database_posts} AS {MockDatabasePost} JOIN {mock_database_tags} AS";
+ $expected .= " {MockDatabaseTag} ON ";
+ $expected .= "MockDatabaseTagging.tag_id = MockDatabaseTag.id";
$expected .= " WHERE MockDatabaseTag.tag IN ('foo', 'bar', 'baz');";
$this->assertEqual($expected, $result);
}
public function testItem() {
- $model = '\lithium\tests\mocks\data\model\MockDatabasePost';
$data = array('title' => 'new post', 'content' => 'This is a new post.');
- $item = $this->db->item($model, $data);
+ $item = $this->db->item($this->_model, $data);
$result = $item->data();
$this->assertEqual($data, $result);
}
public function testCreate() {
- $record = new Record(array(
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ $entity = new Record(array(
+ 'model' => $this->_model,
'data' => array('title' => 'new post', 'body' => 'the body')
));
- $query = new Query(array(
+ $query = new Query(compact('entity') + array(
'type' => 'create',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
- 'record' => $record
+ 'model' => $this->_model,
));
- $expected = sha1(serialize($query));
+ $hash = $query->export($this->db);
+ ksort($hash);
+ $expected = sha1(serialize($hash));
+
$result = $this->db->create($query);
$this->assertTrue($result);
- $result = $query->record()->id;
+ $result = $query->entity()->id;
$this->assertEqual($expected, $result);
- $expected = "INSERT INTO mock_database_posts"
- . " (title, body)"
- . " VALUES ('new post', 'the body');";
+ $expected = "INSERT INTO {mock_database_posts} ({title}, {body})";
+ $expected .= " VALUES ('new post', 'the body');";
$result = $this->db->sql;
$this->assertEqual($expected, $result);
}
public function testCreateWithKey() {
- $record = new Record(array(
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ $entity = new Record(array(
+ 'model' => $this->_model,
'data' => array('id' => 1, 'title' => 'new post', 'body' => 'the body')
));
- $query = new Query(array(
- 'type' => 'create',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
- 'record' => $record
- ));
+ $query = new Query(compact('entity') + array('type' => 'create'));
$expected = 1;
+
$result = $this->db->create($query);
$this->assertTrue($result);
- $result = $query->record()->id;
+ $result = $query->entity()->id;
$this->assertEqual($expected, $result);
- $expected = "INSERT INTO mock_database_posts"
- . " (id, title, body)"
- . " VALUES (1, 'new post', 'the body');";
- $result = $this->db->sql;
- $this->assertEqual($expected, $result);
+ $expected = "INSERT INTO {mock_database_posts} ({id}, {title}, {body})";
+ $expected .= " VALUES (1, 'new post', 'the body');";
+ $this->assertEqual($expected, $this->db->sql);
}
public function testReadWithQueryStringReturnResource() {
- $result = $this->db->read('SELECT * from mock_database_posts;', array(
+ $result = $this->db->read('SELECT * from mock_database_posts AS MockDatabasePost;', array(
'return' => 'resource'
));
$this->assertTrue($result);
- $expected = "SELECT * from mock_database_posts;";
+ $expected = "SELECT * from mock_database_posts AS MockDatabasePost;";
$result = $this->db->sql;
$this->assertEqual($expected, $result);
}
@@ -279,12 +280,12 @@ class DatabaseTest extends \lithium\test\Unit {
public function testReadWithQueryObjectRecordSet() {
$query = new Query(array(
'type' => 'read',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost',
));
$result = $this->db->read($query);
- $this->assertTrue($result instanceof \lithium\data\collection\RecordSet);
+ $this->assertTrue($result instanceof RecordSet);
- $expected = "SELECT * From mock_database_posts;";
+ $expected = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost};";
$result = $this->db->sql;
$this->assertEqual($expected, $result);
}
@@ -292,61 +293,188 @@ class DatabaseTest extends \lithium\test\Unit {
public function testReadWithQueryObjectArray() {
$query = new Query(array(
'type' => 'read',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost',
));
$result = $this->db->read($query, array('return' => 'array'));
$this->assertTrue(is_array($result));
- $expected = "SELECT * From mock_database_posts;";
+ $expected = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost};";
$result = $this->db->sql;
$this->assertEqual($expected, $result);
}
public function testUpdate() {
- $record = new Record(array(
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ $entity = new Record(array(
+ 'model' => $this->_model,
'data' => array('id' => 1, 'title' => 'new post', 'body' => 'the body'),
'exists' => true
));
- $query = new Query(array(
- 'type' => 'update',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
- 'record' => $record
- ));
+ $query = new Query(compact('entity') + array('type' => 'update'));
$result = $this->db->update($query);
+
$this->assertTrue($result);
- $expected = 1;
- $result = $query->record()->id;
- $this->assertEqual($expected, $result);
+ $this->assertEqual(1, $query->entity()->id);
- $expected = "UPDATE mock_database_posts SET"
- . " id = 1, title = 'new post', body = 'the body'"
- . " WHERE id = 1;";
- $result = $this->db->sql;
- $this->assertEqual($expected, $result);
+ $expected = "UPDATE {mock_database_posts} SET";
+ $expected .= " {id} = 1, {title} = 'new post', {body} = 'the body' WHERE id = 1;";
+ $this->assertEqual($expected, $this->db->sql);
}
public function testDelete() {
- $record = new Record(array(
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
+ $entity = new Record(array(
+ 'model' => $this->_model,
'data' => array('id' => 1, 'title' => 'new post', 'body' => 'the body'),
'exists' => true
));
+ $query = new Query(compact('entity') + array('type' => 'delete'));
+ $this->assertTrue($this->db->delete($query));
+ $this->assertEqual(1, $query->entity()->id);
+
+ $expected = "DELETE FROM {mock_database_posts} AS {MockDatabasePost} WHERE id = 1;";
+ $this->assertEqual($expected, $this->db->sql);
+ }
+
+ public function testOrder() {
+ $query = new Query(array('model' => $this->_model));
+
+ $result = $this->db->order("foo_bar", $query);
+ $expected = 'ORDER BY foo_bar ASC';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->db->order("title", $query);
+ $expected = 'ORDER BY {MockDatabasePost}.{title} ASC';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->db->order("title", $query);
+ $expected = 'ORDER BY {MockDatabasePost}.{title} ASC';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->db->order(array("title"), $query);
+ $expected = 'ORDER BY {MockDatabasePost}.{title} ASC';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->db->order(array("title" => "desc"), $query);
+ $expected = 'ORDER BY {MockDatabasePost}.{title} desc';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->db->order(array("title" => "dasc"), $query);
+ $expected = 'ORDER BY {MockDatabasePost}.{title} ASC';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->db->order(array("title" => array()), $query);
+ $expected = 'ORDER BY {MockDatabasePost}.{title} ASC';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->db->order(array('author_id', "title" => "DESC"), $query);
+ $expected = 'ORDER BY {MockDatabasePost}.{author_id} ASC, {MockDatabasePost}.{title} DESC';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testScopedDelete() {
$query = new Query(array(
'type' => 'delete',
- 'model' => '\lithium\tests\mocks\data\model\MockDatabasePost',
- 'record' => $record
+ 'conditions' => array('published' => false),
+ 'model' => $this->_model
));
- $result = $this->db->delete($query);
- $this->assertTrue($result);
- $expected = 1;
- $result = $query->record()->id;
- $this->assertEqual($expected, $result);
+ $sql = 'DELETE FROM {mock_database_posts} AS {MockDatabasePost} WHERE published = 0;';
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+ }
- $expected = "DELETE From mock_database_posts"
- . " WHERE id = 1;";
- $result = $this->db->sql;
- $this->assertEqual($expected, $result);
+ public function testScopedUpdate() {
+ $query = new Query(array(
+ 'type' => 'update',
+ 'conditions' => array('expires' => array('>=' => '2010-05-13')),
+ 'data' => array('published' => false),
+ 'model' => $this->_model
+ ));
+ $sql = "UPDATE {mock_database_posts} SET {published} = 0 WHERE ";
+ $sql .= "({expires} >= '2010-05-13');";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+ }
+
+ public function testQueryOperators() {
+ $query = new Query(array('type' => 'read', 'model' => $this->_model, 'conditions' => array(
+ 'score' => array('between' => array(90, 100))
+ )));
+ $sql = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost} WHERE ({score} ";
+ $sql .= "BETWEEN 90 AND 100);";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+
+ $query = new Query(array('type' => 'read', 'model' => $this->_model, 'conditions' => array(
+ 'score' => array('>' => 90, '<' => 100)
+ )));
+ $sql = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost} WHERE ";
+ $sql .= "({score} > 90 AND {score} < 100);";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+
+ $query = new Query(array('type' => 'read', 'model' => $this->_model, 'conditions' => array(
+ 'score' => array('!=' => array(98, 99, 100))
+ )));
+ $sql = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost} ";
+ $sql .= "WHERE ({score} NOT IN (98, 99, 100));";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+
+ $query = new Query(array('type' => 'read', 'model' => $this->_model, 'conditions' => array(
+ 'scorer' => array('like' => '%howard%')
+ )));
+ $sql = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost} ";
+ $sql .= "WHERE ({scorer} like '%howard%');";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+
+ $conditions = "custom conditions string";
+ $query = new Query(compact('conditions') + array(
+ 'type' => 'read', 'model' => $this->_model
+ ));
+ $sql = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost} WHERE {$conditions};";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+
+ $query = new Query(array(
+ 'type' => 'read', 'model' => $this->_model,
+ 'conditions' => array(
+ 'field' => array('like' => '%value%')
+ )
+ ));
+ $sql = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost} WHERE ";
+ $sql .= "({field} like '%value%');";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+
+ $query = new Query(array(
+ 'type' => 'read', 'model' => $this->_model,
+ 'conditions' => array(
+ 'or' => array(
+ 'field1' => 'value1',
+ 'field2' => 'value2',
+ 'and' => array('sField' => '1', 'sField2' => '2')
+ ),
+ 'bField' => '3'
+ )
+ ));
+ $sql = "SELECT * FROM {mock_database_posts} AS {MockDatabasePost} WHERE ";
+ $sql .= "({field1} = 'value1' OR {field2} = 'value2' OR ({sField} = 1 AND {sField2} = 2))";
+ $sql .= " AND {bField} = 3;";
+ $this->assertEqual($sql, $this->db->renderCommand($query));
+ }
+
+ public function testRawConditions() {
+ $query = new Query(array('type' => 'read', 'model' => $this->_model, 'conditions' => null));
+ $this->assertFalse($this->db->conditions(5, $query));
+ $this->assertFalse($this->db->conditions(null, $query));
+ $this->assertEqual("WHERE CUSTOM", $this->db->conditions("CUSTOM", $query));
+ }
+
+ /**
+ * Tests that various syntaxes for the `'order'` key of the query object produce the correct
+ * SQL.
+ *
+ * @return void
+ */
+ public function testQueryOrderSyntaxes() {
+ $query = new Query(array(
+ 'type' => 'read', 'model' => $this->_model, 'order' => array('created' => 'ASC')
+ ));
+ $sql = 'SELECT * FROM {mock_database_posts} AS {MockDatabasePost} ';
+ $sql .= 'ORDER BY {MockDatabasePost}.{created} ASC;';
+ $this->assertEqual($sql, $this->db->renderCommand($query));
}
}
diff --git a/libraries/lithium/tests/cases/data/source/HttpTest.php b/libraries/lithium/tests/cases/data/source/HttpTest.php
index eb14641..72af2ea 100644
--- a/libraries/lithium/tests/cases/data/source/HttpTest.php
+++ b/libraries/lithium/tests/cases/data/source/HttpTest.php
@@ -2,20 +2,22 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data\source;
-use \lithium\data\source\Http;
+use lithium\data\source\Http;
+use lithium\data\Connections;
+use lithium\data\model\Query;
class HttpTest extends \lithium\test\Unit {
+ protected $_model = 'lithium\tests\mocks\data\source\MockHttpModel';
+
protected $_testConfig = array(
- 'classes' => array(
- 'socket' => '\lithium\tests\mocks\data\source\http\adapter\MockSocket'
- ),
+ 'classes' => array('response' => 'lithium\net\http\Response'),
'persistent' => false,
'protocol' => 'tcp',
'host' => 'localhost',
@@ -23,16 +25,41 @@ class HttpTest extends \lithium\test\Unit {
'password' => '',
'port' => 80,
'timeout' => 2,
+ 'socket' => 'lithium\tests\mocks\data\source\http\adapter\MockSocket'
);
+ public function setUp() {
+ $this->_configs = Connections::config();
+ Connections::reset();
+
+ Connections::config(array(
+ 'mock-http-connection' => array('type' => 'Http')
+ ));
+
+ Connections::config(array(
+ 'mock-http-conn' => array(
+ 'type' => 'Http',
+ 'methods' => array(
+ 'something' => array('method' => 'get'),
+ 'do' => array('method' => 'post'),
+ )
+ )
+ ));
+ }
+
+ public function tearDown() {
+ Connections::reset();
+ Connections::config($this->_configs);
+ unset($this->query);
+ }
+
public function testAllMethodsNoConnection() {
- $http = new Http(array('classes' => array('socket' => false)));
- $this->assertFalse($http->connect());
+ $http = new Http(array('socket' => false));
+ $this->assertTrue($http->connect());
$this->assertTrue($http->disconnect());
$this->assertFalse($http->get());
$this->assertFalse($http->post());
$this->assertFalse($http->put());
- $this->assertFalse($http->delete());
}
public function testConnect() {
@@ -57,69 +84,57 @@ class HttpTest extends \lithium\test\Unit {
public function testDescribe() {
$http = new Http($this->_testConfig);
- $result = $http->describe(null, null);
+ $result = $http->describe(null, array());
}
public function testGet() {
$http = new Http($this->_testConfig);
$result = $http->get();
- $this->assertEqual('Test!', $result);
- $expected = 'HTTP/1.1';
$result = $http->last->response->protocol;
- $this->assertEqual($expected, $result);
+ $this->assertEqual('HTTP/1.1', $result);
- $expected = '200';
$result = $http->last->response->status['code'];
- $this->assertEqual($expected, $result);
+ $this->assertEqual('200', $result);
- $expected = 'OK';
$result = $http->last->response->status['message'];
- $this->assertEqual($expected, $result);
+ $this->assertEqual('OK', $result);
- $expected = 'text/html';
$result = $http->last->response->type;
- $this->assertEqual($expected, $result);
+ $this->assertEqual('text/html', $result);
- $expected = 'UTF-8';
- $result = $http->last->response->charset;
- $this->assertEqual($expected, $result);
+ $result = $http->last->response->encoding;
+ $this->assertEqual('UTF-8', $result);
}
public function testGetPath() {
$http = new Http($this->_testConfig);
$result = $http->get('search.json');
- $this->assertEqual('Test!', $result);
- $expected = 'HTTP/1.1';
$result = $http->last->response->protocol;
- $this->assertEqual($expected, $result);
+ $this->assertEqual('HTTP/1.1', $result);
- $expected = '200';
$result = $http->last->response->status['code'];
- $this->assertEqual($expected, $result);
+ $this->assertEqual('200', $result);
- $expected = 'OK';
$result = $http->last->response->status['message'];
- $this->assertEqual($expected, $result);
+ $this->assertEqual('OK', $result);
- $expected = 'text/html';
$result = $http->last->response->type;
- $this->assertEqual($expected, $result);
+ $this->assertEqual('text/html', $result);
- $expected = 'UTF-8';
- $result = $http->last->response->charset;
- $this->assertEqual($expected, $result);
+ $result = $http->last->response->encoding;
+ $this->assertEqual('UTF-8', $result);
}
public function testPost() {
$http = new Http($this->_testConfig);
- $http->post('update.xml', array('status' => 'cool'));
+ $http->post('add.xml', array('status' => 'cool'));
$expected = join("\r\n", array(
- 'POST /update.xml HTTP/1.1',
+ 'POST /add.xml HTTP/1.1',
'Host: localhost:80',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'Content-Type: application/x-www-form-urlencoded',
'Content-Length: 11',
'', 'status=cool'
@@ -130,32 +145,200 @@ class HttpTest extends \lithium\test\Unit {
public function testPut() {
$http = new Http($this->_testConfig);
- $result = $http->put();
- $this->assertEqual('Test!', $result);
- }
-
- public function testDelete() {
- $http = new Http($this->_testConfig);
- $result = $http->delete(null);
- $this->assertEqual('Test!', $result);
+ $result = $http->put('update.xml', array('status' => 'cool'));
+ $expected = join("\r\n", array(
+ 'PUT /update.xml HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: 11',
+ '', 'status=cool'
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
}
public function testCreate() {
$http = new Http($this->_testConfig);
$result = $http->create(null);
- $this->assertEqual('Test!', $result);
+ $expected = join("\r\n", array(
+ 'POST / HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
}
public function testRead() {
$http = new Http($this->_testConfig);
$result = $http->read(null);
- $this->assertEqual('Test!', $result);
+ $expected = join("\r\n", array(
+ 'GET / HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
}
public function testUpdate() {
$http = new Http($this->_testConfig);
$result = $http->update(null);
- $this->assertEqual('Test!', $result);
+ $expected = join("\r\n", array(
+ 'PUT / HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testDelete() {
+ $http = new Http($this->_testConfig);
+ $result = $http->delete(null);
+ $expected = join("\r\n", array(
+ 'DELETE / HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCreateWithModel() {
+ $http = new Http($this->_testConfig);
+ $query = new Query(array(
+ 'model' => $this->_model,
+ 'data' => array('title' => 'Test Title')
+ ));
+
+ $result = $http->create($query);
+ $expected = join("\r\n", array(
+ 'POST /posts HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: 16',
+ '', 'title=Test+Title'
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testReadWithModel() {
+ $http = new Http($this->_testConfig);
+ $query = new Query(array('model' => $this->_model));
+
+ $result = $http->read($query);
+ $expected = join("\r\n", array(
+ 'GET /posts HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testReadWithModelConditions() {
+ $http = new Http($this->_testConfig);
+ $query = new Query(array(
+ 'model' => $this->_model,
+ 'conditions' => array('page' => 2)
+ ));
+
+ $result = $http->read($query);
+ $expected = join("\r\n", array(
+ 'GET /posts?page=2 HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/x-www-form-urlencoded',
+ '', ''
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testUpdateWithModel() {
+ $http = new Http($this->_testConfig);
+ $query = new Query(array(
+ 'model' => $this->_model,
+ 'data' => array('id' => '1', 'title' => 'Test Title')
+ ));
+
+ $result = $http->update($query);
+ $expected = join("\r\n", array(
+ 'PUT /posts/1 HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: 21',
+ '', 'id=1&title=Test+Title'
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testDeleteWithModel() {
+ $http = new Http($this->_testConfig);
+ $query = new Query(array('model' => $this->_model, 'data' => array('id' => '1')));
+
+ $result = $http->delete($query);
+ $expected = join("\r\n", array(
+ 'DELETE /posts/1 HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $http->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCustomGetMethod() {
+ $conn = Connections::get('mock-http-conn');
+
+ $result = $conn->something();
+ $expected = join("\r\n", array(
+ 'GET /something HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $conn->last->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCustomPostMethod() {
+ $conn = Connections::get('mock-http-conn');
+
+ $result = $conn->do(array('title' => 'sup'));
+ $expected = join("\r\n", array(
+ 'POST /do HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: 9',
+ '', 'title=sup'
+ ));
+ $result = (string) $conn->last->request;
+ $this->assertEqual($expected, $result);
}
}
diff --git a/libraries/lithium/tests/cases/data/source/MongoDbTest.php b/libraries/lithium/tests/cases/data/source/MongoDbTest.php
index d3e3cc9..7d13921 100644
--- a/libraries/lithium/tests/cases/data/source/MongoDbTest.php
+++ b/libraries/lithium/tests/cases/data/source/MongoDbTest.php
@@ -2,14 +2,661 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
- *
*/
namespace lithium\tests\cases\data\source;
+use lithium\data\source\MongoDb;
+use Exception;
+use MongoId;
+use MongoCode;
+use MongoDate;
+use MongoRegex;
+use MongoMaxKey;
+use lithium\data\Model;
+use lithium\data\Connections;
+use lithium\data\model\Query;
+use lithium\data\entity\Document;
+use lithium\tests\mocks\data\MockPost;
+use lithium\data\collection\DocumentSet;
+use lithium\data\collection\DocumentArray;
+use lithium\tests\mocks\data\source\MockMongoSource;
+use lithium\tests\mocks\data\source\MockMongoConnection;
+
class MongoDbTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\source\MockMongoPost';
+
+ protected $_testConfig = array(
+ 'type' => 'MongoDb',
+ 'adapter' => false,
+ 'database' => 'lithium_test',
+ 'host' => 'localhost',
+ 'port' => '27017',
+ 'persistent' => null,
+ 'autoConnect' => false
+ );
+
+ protected $_schema = array(
+ '_id' => array('type' => 'id'),
+ 'guid' => array('type' => 'id'),
+ 'title' => array('type' => 'string'),
+ 'tags' => array('type' => 'string', 'array' => true),
+ 'comments' => array('type' => 'MongoId'),
+ 'authors' => array('type' => 'MongoId', 'array' => true),
+ 'created' => array('type' => 'MongoDate'),
+ 'modified' => array('type' => 'datetime'),
+ 'voters' => array('type' => 'id', 'array' => true),
+ 'rank_count' => array('type' => 'integer', 'default' => 0),
+ 'rank' => array('type' => 'float', 'default' => 0.0),
+ 'notifications.foo' => array('type' => 'boolean'),
+ 'notifications.bar' => array('type' => 'boolean'),
+ 'notifications.baz' => array('type' => 'boolean'),
+ );
+
+ protected $_configs = array();
+
+ public function skip() {
+ $this->skipIf(!MongoDb::enabled(), 'MongoDb is not enabled');
+
+ $db = new MongoDb($this->_testConfig);
+ $message = "`{$this->_testConfig['database']}` database or connection unavailable";
+ $this->skipIf(!$db->isConnected(array('autoConnect' => true)), $message);
+ }
+
+ /**
+ * This hack is a necessary optimization until these tests are properly mocked out.
+ *
+ * @param array $options Options for the parent class' method.
+ * @return void
+ */
+ public function run(array $options = array()) {
+ $this->_results = array();
+
+ try {
+ $this->skip();
+ } catch (Exception $e) {
+ $this->_handleException($e);
+ return $this->_results;
+ }
+ $this->_configs = Connections::config();
+ $result = parent::run($options);
+ Connections::get('lithium_mongo_test')->dropDB('lithium_test');
+ Connections::reset();
+ Connections::config($this->_configs);
+ return $result;
+ }
+
+ public function setUp() {
+ Connections::config(array('lithium_mongo_test' => $this->_testConfig));
+ $this->db = Connections::get('lithium_mongo_test');
+ $model = $this->_model;
+ $model::config(array('key' => '_id'));
+ $model::resetConnection(false);
+
+ $this->query = new Query(compact('model') + array(
+ 'entity' => new Document(compact('model'))
+ ));
+ }
+
+ public function tearDown() {
+ try {
+ $this->db->delete($this->query);
+ } catch (Exception $e) {}
+ unset($this->query);
+ }
+
+ public function testBadConnection() {
+ $db = new MongoDb(array('host' => null, 'autoConnect' => false));
+ $this->expectException('Could not connect to the database.');
+ $this->assertFalse($db->connect());
+ $this->assertTrue($db->disconnect());
+ }
+
+ public function testGoodConnectionBadDatabase() {
+ $this->expectException('Could not connect to the database.');
+ $db = new MongoDb(array('database' => null, 'autoConnnect' => false));
+ }
+
+ public function testGoodConnectionGoodDatabase() {
+ $db = new MongoDb(array('autoConnect' => false) + $this->_testConfig);
+ $this->assertFalse($db->isConnected());
+ $this->assertTrue($db->connect());
+ $this->assertTrue($db->isConnected());
+ }
+
+ public function testEntities() {
+ $result = $this->db->entities();
+ $expected = array();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testDescribe() {
+ $result = $this->db->describe('test');
+ $expected = array();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testName() {
+ $result = $this->db->name('{(\'Li\':"∆")}');
+ $expected = '{(\'Li\':"∆")}';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSchema() {
+ $result = $this->db->schema($this->query);
+ $expected = array();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCreateFail() {
+ $this->expectException('no elements in doc');
+ $result = $this->db->create($this->query);
+ }
+
+ public function testCreateSuccess() {
+ $this->query->data(array('title' => 'Test Post'));
+ $result = $this->db->create($this->query);
+ $this->assertTrue($result);
+ }
+
+ public function testConditions() {
+ $result = $this->db->conditions(null, null);
+ $this->assertEqual(array(), $result);
+
+ $function = 'function() { return this.x < y;}';
+ $conditions = new MongoCode($function);
+ $result = $this->db->conditions($conditions, null);
+
+ $this->assertTrue(is_array($result));
+ $this->assertTrue(isset($result['$where']));
+ $this->assertEqual($conditions, $result['$where']);
+
+ $conditions = $function;
+ $result = $this->db->conditions($conditions, null);
+ $this->assertTrue(is_array($result));
+ $this->assertTrue(isset($result['$where']));
+ $this->assertEqual($conditions, $result['$where']);
+
+ $conditions = array('key' => 'value', 'anotherkey' => 'some other value');
+ $result = $this->db->conditions($conditions, null);
+ $this->assertTrue(is_array($result));
+ $this->assertEqual($conditions, $result);
+
+ $conditions = array('key' => array('one', 'two', 'three'));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertTrue(is_array($result));
+ $this->assertTrue(isset($result['key']));
+ $this->assertTrue(isset($result['key']['$in']));
+ $this->assertEqual($conditions['key'], $result['key']['$in']);
+ }
+
+ public function testMongoConditionalOperators() {
+ $conditions = array('key' => array('<' => 10));
+ $expected = array('key' => array('$lt' => 10));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('<=' => 10));
+ $expected = array('key' => array('$lte' => 10));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('>' => 10));
+ $expected = array('key' => array('$gt' => 10));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('>=' => 10));
+ $expected = array('key' => array('$gte' => 10));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('!=' => 10));
+ $expected = array('key' => array('$ne' => 10));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('<>' => 10));
+ $expected = array('key' => array('$ne' => 10));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('!=' => array(10, 20, 30)));
+ $expected = array('key' => array('$nin' => array(10, 20, 30)));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('<>' => array(10, 20, 30)));
+ $expected = array('key' => array('$nin' => array(10, 20, 30)));
+ $result = $this->db->conditions($conditions, null);
+ $this->assertEqual($expected, $result);
+
+ $conditions = array('key' => array('like' => '/regex/i'));
+ $result = $this->db->conditions($conditions, null);
+ $expected = array('key' => new MongoRegex('/regex/i'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testReadNoConditions() {
+ $this->db->connect();
+ $connection = $this->db->connection;
+ $this->db->connection = new MockMongoSource();
+ $this->db->connection->resultSets = array(array('ok' => true));
+
+ $data = array('title' => 'Test Post');
+ $options = array('safe' => false, 'fsync' => false);
+ $this->query->data($data);
+ $this->assertIdentical(true, $this->db->create($this->query));
+ $this->assertEqual(compact('data', 'options'), end($this->db->connection->queries));
+
+ $this->db->connection->resultSets = array(array(array('_id' => new MongoId()) + $data));
+ $result = $this->db->read($this->query);
+
+ $this->assertTrue($result instanceof DocumentSet);
+ $this->assertEqual(1, $result->count());
+ $this->assertEqual('Test Post', $result->first()->title);
+ $this->db->connection = $connection;
+ }
+
+ public function testReadWithConditions() {
+ $this->db->connect();
+ $connection = $this->db->connection;
+ $this->db->connection = new MockMongoSource();
+ $this->db->connection->resultSets = array(array('ok' => true));
+
+ $data = array('title' => 'Test Post');
+ $options = array('safe' => false, 'fsync' => false);
+ $this->query->data($data);
+ $this->assertTrue($this->db->create($this->query));
+ $this->query->data(null);
+
+ $this->db->connection->resultSets = array(array());
+ $this->query->conditions(array('title' => 'Nonexistent Post'));
+ $result = $this->db->read($this->query);
+ $this->assertTrue($result == true);
+ $this->assertEqual(0, $result->count());
+
+ $this->db->connection->resultSets = array(array($data));
+ $this->query->conditions($data);
+ $result = $this->db->read($this->query);
+ $this->assertTrue($result == true);
+ $this->assertEqual(1, $result->count());
+ $this->db->connection = $connection;
+ }
+
+ public function testUpdate() {
+ $model = $this->_model;
+
+ $this->query->model($model);
+ $this->query->data(array('title' => 'Test Post'));
+ $this->db->create($this->query);
+
+ $result = $this->db->read(new Query(compact('model')));
+ $original = $result->first()->to('array');
+
+ $this->assertEqual(array('_id', 'title'), array_keys($original));
+ $this->assertEqual('Test Post', $original['title']);
+ $this->assertPattern('/^[0-9a-f]{24}$/', $original['_id']);
+
+ $this->query = new Query(compact('model') + array(
+ 'data' => array('title' => 'New Post Title'),
+ 'conditions' => array('_id' => $original['_id'])
+ ));
+ $this->assertTrue($this->db->update($this->query));
+
+ $result = $this->db->read(new Query(compact('model') + array(
+ 'conditions' => array('_id' => $original['_id'])
+ )));
+ $this->assertEqual(1, $result->count());
+
+ $updated = $result->first();
+ $updated = $updated ? $updated->to('array') : array();
+ $this->assertEqual($original['_id'], $updated['_id']);
+ $this->assertEqual('New Post Title', $updated['title']);
+ }
+
+ public function testDelete() {
+ $data = array('title' => 'Delete Me');
+ $this->query->data($data);
+ $this->db->create($this->query);
+
+ $result = $this->db->read($this->query);
+ $expected = 1;
+ $this->assertEqual($expected, $result->count());
+
+ $record = $result->first()->to('array');
+
+ $model = $this->_model;
+ $this->query = new Query(compact('model') + array(
+ 'entity' => new Document(compact('model'))
+ ));
+ $this->query->conditions(array('_id' => $record['_id']));
+ $result = $this->db->delete($this->query);
+
+ $this->assertTrue($result);
+
+ $result = $this->db->read($this->query);
+ $this->assertTrue($result);
+
+ $expected = 0;
+ $this->assertEqual($expected, $result->count());
+ }
+
+ public function testItem() {
+ $model = $this->_model;
+ $data = array('title' => 'New Item');
+ $result = $this->db->item($model, $data);
+
+ $this->assertTrue($result instanceof \lithium\data\entity\Document);
+
+ $expected = $data;
+ $result = $result->to('array');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCalculation() {
+ $result = $this->db->calculation('count', $this->query);
+ $expected = 0;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testEnabled() {
+ $this->assertTrue(MongoDb::enabled());
+ $this->assertTrue(MongoDb::enabled('arrays'));
+ $this->assertTrue(MongoDb::enabled('booleans'));
+ $this->assertTrue(MongoDb::enabled('relationships'));
+ }
+
+ public function testArbitraryMethodCalls() {
+ $config = $this->_testConfig;
+ $result = $this->db->__toString();
+ $this->assertTrue(strpos($result, $config['host']) !== false);
+ $this->assertTrue(strpos($result, $config['port']) !== false);
+ $this->assertTrue(is_array($this->db->listDBs()));
+ }
+
+ public function testDocumentSorting() {
+ $model = $this->_model;
+ $model::config(array('connection' => 'lithium_mongo_test', 'source' => 'ordered_docs'));
+
+ $model::create(array('title' => 'Third document', 'position' => 3))->save();
+ $model::create(array('title' => 'First document', 'position' => 1))->save();
+ $model::create(array('title' => 'Second document', 'position' => 2))->save();
+
+ $documents = $model::all(array('order' => 'position'));
+
+ $this->assertEqual('First document', $documents[0]->title);
+ $this->assertEqual('Second document', $documents[1]->title);
+ $this->assertEqual('Third document', $documents[2]->title);
+
+ $documents = $model::all(array('order' => array('position' => 'asc')));
+
+ $this->assertEqual('First document', $documents[0]->title);
+ $this->assertEqual('Second document', $documents[1]->title);
+ $this->assertEqual('Third document', $documents[2]->title);
+
+ $copy = $model::all(array('order' => array('position')));
+ $this->assertIdentical($documents->data(), $copy->data());
+
+ $documents = $model::all(array('order' => array('position' => 'desc')));
+
+ $this->assertEqual('Third document', $documents[0]->title);
+ $this->assertEqual('Second document', $documents[1]->title);
+ $this->assertEqual('First document', $documents[2]->title);
+
+ $list = $model::find('list');
+ $this->assertEqual(3, count($list));
+
+ foreach ($list as $id => $title) {
+ $this->assertTrue(is_string($id));
+ $this->assertPattern('/^[a-f0-9]{24}$/', $id);
+ $this->assertNull($title);
+ }
+ $model::config(array('title' => 'title'));
+
+ $list = $model::find('list');
+ $this->assertEqual(3, count($list));
+
+ foreach ($list as $id => $title) {
+ $this->assertTrue(is_string($id));
+ $this->assertPattern('/^[a-f0-9]{24}$/', $id);
+ $this->assertPattern('/^(First|Second|Third) document$/', $title);
+ }
+
+ foreach ($documents as $i => $doc) {
+ $this->assertTrue($doc->delete());
+ }
+ }
+
+ public function testMongoIdPreservation() {
+ $model = $this->_model;
+ $model::config(array('connection' => 'lithium_mongo_test', 'source' => 'ordered_docs'));
+
+ $post = $model::create(array('title' => 'A post'));
+ $post->save();
+ $id = $post->_id;
+
+ $data = Connections::get('lithium_mongo_test')->connection->ordered_docs->findOne(array(
+ '_id' => $id
+ ));
+ $this->assertEqual('A post', $data['title']);
+ $this->assertEqual($id, (string) $data['_id']);
+ $this->assertTrue($data['_id'] instanceof MongoId);
+
+ $post->title = 'An updated post';
+ $post->save();
+
+ $data = Connections::get('lithium_mongo_test')->connection->ordered_docs->findOne(array(
+ '_id' => new MongoId($id)
+ ));
+ $this->assertEqual('An updated post', $data['title']);
+ $this->assertEqual($id, (string) $data['_id']);
+ }
+
+ public function testRelationshipGeneration() {
+ Connections::add('mock-source', $this->_testConfig);
+ $from = 'lithium\tests\mocks\data\MockComment';
+ $to = 'lithium\tests\mocks\data\MockPost';
+
+ $from::config(array('connection' => 'mock-source'));
+ $to::config(array('connection' => 'mock-source', 'key' => '_id'));
+
+ $result = $this->db->relationship($from, 'belongsTo', 'MockPost');
+ $expected = array(
+ 'name' => 'MockPost',
+ 'type' => 'belongsTo',
+ 'keys' => array('mockComment' => '_id'),
+ 'from' => $from,
+ 'link' => 'contained',
+ 'to' => $to,
+ 'fields' => true,
+ 'fieldName' => 'mockPost',
+ 'constraint' => null,
+ 'init' => true
+ );
+ $this->assertEqual($expected, $result->data());
+ Connections::config(array('mock-source' => false));
+ }
+
+ public function testCreateNoConnectionException() {
+ $db = new MockMongoConnection($this->_testConfig + array('autoConnect' => false));
+ $this->expectException('Could not connect to the database.');
+ $result = $db->create(null);
+ }
+
+ public function testReadNoConnectionException() {
+ $db = new MockMongoConnection($this->_testConfig + array('autoConnect' => false));
+ $this->expectException('Could not connect to the database.');
+ $result = $db->read(null);
+ }
+
+ public function testUpdateNoConnectionException() {
+ $db = new MockMongoConnection($this->_testConfig + array('autoConnect' => false));
+ $this->expectException('Could not connect to the database.');
+ $result = $db->update(null);
+ }
+
+ public function testDeleteNoConnectionException() {
+ $db = new MockMongoConnection($this->_testConfig + array('autoConnect' => false));
+ $this->expectException('Could not connect to the database.');
+ $result = $db->delete(null);
+ }
+
+ public function testEntitiesNoConnectionException() {
+ $db = new MockMongoConnection($this->_testConfig + array('autoConnect' => false));
+ $this->expectException('Could not connect to the database.');
+ $result = $db->entities(null);
+ }
+
+ public function testAtomicUpdate() {
+ $model = $this->_model;
+ $model::config(array('connection' => 'lithium_mongo_test', 'source' => 'posts'));
+
+ $document = $model::create(array('initial' => 'one', 'values' => 'two'));
+ $document->save();
+
+ $duplicate = $model::create(array('_id' => $document->_id), array('exists' => true));
+ $duplicate->values = 'new';
+ $this->assertTrue($duplicate->save());
+
+ $document = $model::find((string) $duplicate->_id);
+ $expected = array(
+ '_id' => (string) $duplicate->_id, 'initial' => 'one', 'values' => 'new'
+ );
+ $this->assertEqual($expected, $document->data());
+ }
+
+ /**
+ * Tests that the MongoDB adapter will not attempt to overwrite the _id field on document
+ * update.
+ *
+ * @return void
+ */
+ public function testPreserveId() {
+ $model = $this->_model;
+ $model::config(array('connection' => 'lithium_mongo_test', 'source' => 'posts'));
+
+ $document = $model::create(array('_id' => 'custom'));
+ $document->save();
+
+ $document->_id = 'custom2';
+ $document->foo = 'bar';
+ $this->assertTrue($document->save());
+ $this->assertNull($model::first('custom2'));
+ $this->assertEqual(array('_id' => 'custom'), $model::first('custom')->data());
+ }
+
+ public function testCastingConditionsValues() {
+ $query = new Query(array('schema' => $this->_schema));
+
+ $conditions = array('_id' => new MongoId("4c8f86167675abfabdbe0300"));
+ $result = $this->db->conditions($conditions, $query);
+ $this->assertEqual($conditions, $result);
+
+ $conditions = array('_id' => "4c8f86167675abfabdbe0300");
+ $result = $this->db->conditions($conditions, $query);
+
+ $this->assertEqual(array_keys($conditions), array_keys($result));
+ $this->assertTrue($result['_id'] instanceof MongoId);
+ $this->assertEqual($conditions['_id'], (string) $result['_id']);
+
+ $conditions = array('_id' => array(
+ "4c8f86167675abfabdbe0300", "4c8f86167675abfabdbf0300", "4c8f86167675abfabdc00300"
+ ));
+ $result = $this->db->conditions($conditions, $query);
+ $this->assertEqual(3, count($result['_id']['$in']));
+ $this->assertTrue($result['_id']['$in'][0] instanceof MongoId);
+ $this->assertTrue($result['_id']['$in'][1] instanceof MongoId);
+ $this->assertTrue($result['_id']['$in'][2] instanceof MongoId);
+
+ $conditions = array('voters' => array('$all' => array(
+ "4c8f86167675abfabdbf0300", "4c8f86167675abfabdc00300"
+ )));
+ $result = $this->db->conditions($conditions, $query);
+
+ $this->assertEqual(2, count($result['voters']['$all']));
+ $this->assertTrue($result['voters']['$all'][0] instanceof MongoId);
+ $this->assertTrue($result['voters']['$all'][1] instanceof MongoId);
+
+ $conditions = array('$or' => array(
+ array('_id' => "4c8f86167675abfabdbf0300"),
+ array('guid' => "4c8f86167675abfabdbf0300")
+ ));
+ $result = $this->db->conditions($conditions, $query);
+ $this->assertEqual(array('$or'), array_keys($result));
+ $this->assertEqual(2, count($result['$or']));
+ $this->assertTrue($result['$or'][0]['_id'] instanceof MongoId);
+ $this->assertTrue($result['$or'][1]['guid'] instanceof MongoId);
+ }
+
+ public function testMultiOperationConditions() {
+ $conditions = array('loc' => array('$near' => array(50, 50), '$maxDistance' => 5));
+ $result = $this->db->conditions($conditions, $this->query);
+ $this->assertEqual($conditions, $result);
+ }
+
+ public function testCreateWithEmbeddedObjects() {
+ $data = array(
+ '_id' => new MongoId(),
+ 'created' => new MongoDate(strtotime('-1 hour')),
+ 'list' => array('foo', 'bar', 'baz')
+ );
+ $entity = new Document(compact('data') + array('exists' => false));
+ $query = new Query(array('type' => 'create') + compact('entity'));
+ $result = $query->export($this->db);
+ $this->assertIdentical($data, $result['data']['data']);
+ }
+
+ public function testUpdateWithEmbeddedObjects() {
+ $data = array(
+ '_id' => new MongoId(),
+ 'created' => new MongoDate(strtotime('-1 hour')),
+ 'list' => array('foo', 'bar', 'baz')
+ );
+ $model = $this->_model;
+ $schema = array('updated' => array('type' => 'MongoDate'));
+ $entity = new Document(compact('data', 'schema', 'model') + array('exists' => true));
+ $entity->updated = time();
+ $entity->list[] = 'dib';
+
+ $query = new Query(array('type' => 'update') + compact('entity'));
+ $result = $query->export($this->db);
+ $this->assertEqual(array('updated'), array_keys($result['data']['update']));
+ $this->assertTrue($result['data']['update']['updated'] instanceof MongoDate);
+ }
+
+ /**
+ * Assert that Mongo and the Mongo Exporter don't mangle manual geospatial queries.
+ *
+ * @return void
+ */
+ public function testGeoQueries() {
+ $coords = array(84.13, 11.38);
+ $coords2 = array_map(function($point) { return $point + 5; }, $coords);
+ $conditions = array('location' => array('$near' => $coords));
+
+ $query = new Query(compact('conditions') + array('model' => $this->_model));
+ $result = $query->export($this->db);
+ $this->assertEqual($result['conditions'], $conditions);
+
+ $conditions = array('location' => array(
+ '$within' => array('$box' => array($coords2, $coords))
+ ));
+ $query = new Query(compact('conditions') + array('model' => $this->_model));
+ $result = $query->export($this->db);
+ $this->assertEqual($conditions, $result['conditions']);
+ }
+
+ public function testSchemaCallback() {
+ $schema = array('_id' => array('type' => 'id'), 'created' => array('type' => 'date'));
+ $db = new MongoDb(array('autoConnect' => false, 'schema' => function() use ($schema) {
+ return $schema;
+ }));
+ $this->assertEqual($schema, $db->describe(null));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/source/database/adapter/MySqlTest.php b/libraries/lithium/tests/cases/data/source/database/adapter/MySqlTest.php
index f4e0ce2..0b4ce2c 100644
--- a/libraries/lithium/tests/cases/data/source/database/adapter/MySqlTest.php
+++ b/libraries/lithium/tests/cases/data/source/database/adapter/MySqlTest.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data\source\database\adapter;
-use \lithium\data\Connections;
-use \lithium\data\source\database\adapter\MySql;
-use \lithium\tests\mocks\data\source\database\adapter\MockMySql;
+use lithium\data\Connections;
+use lithium\data\model\Query;
+use lithium\data\source\database\adapter\MySql;
+use lithium\tests\mocks\data\source\database\adapter\MockMySql;
class MySqlTest extends \lithium\test\Unit {
@@ -25,6 +26,8 @@ class MySqlTest extends \lithium\test\Unit {
* @todo Tie into the Environment class to ensure that the test database is being used.
*/
public function skip() {
+ $this->skipIf(!MySql::enabled(), 'MySQL Extension is not loaded');
+
$this->_dbConfig = Connections::get('test', array('config' => true));
$hasDb = (isset($this->_dbConfig['adapter']) && $this->_dbConfig['adapter'] == 'MySql');
$message = 'Test database is either unavailable, or not using a MySQL adapter';
@@ -44,9 +47,9 @@ class MySqlTest extends \lithium\test\Unit {
$db = new MockMySql(array('autoConnect' => false));
$result = $db->get('_config');
$expected = array(
- 'autoConnect' => false, 'port' => '3306', 'encoding' => NULL,'persistent' => true,
- 'host' => 'localhost', 'login' => 'root', 'password' => '',
- 'database' => 'lithium', 'init' => true
+ 'autoConnect' => false, 'encoding' => NULL,'persistent' => true,
+ 'host' => 'localhost:3306', 'login' => 'root', 'password' => '',
+ 'database' => null, 'init' => true
);
$this->assertEqual($expected, $result);
}
@@ -59,11 +62,24 @@ class MySqlTest extends \lithium\test\Unit {
*/
public function testDatabaseConnection() {
$db = new MySql(array('autoConnect' => false) + $this->_dbConfig);
+
$this->assertTrue($db->connect());
$this->assertTrue($db->isConnected());
$this->assertTrue($db->disconnect());
$this->assertFalse($db->isConnected());
+
+ $db = new MySQL(array(
+ 'autoConnect' => false, 'encoding' => NULL,'persistent' => false,
+ 'host' => 'localhost:3306', 'login' => 'garbage', 'password' => '',
+ 'database' => 'garbage', 'init' => true
+ ) + $this->_dbConfig);
+
+ $this->assertFalse($db->connect());
+ $this->assertFalse($db->isConnected());
+
+ $this->assertTrue($db->disconnect());
+ $this->assertFalse($db->isConnected());
}
public function testDatabaseEncoding() {
@@ -86,58 +102,149 @@ class MySqlTest extends \lithium\test\Unit {
$this->assertTrue(is_string($result));
$this->assertEqual($expected, $result);
- $expected = true;
- $result = $this->db->value(true);
- $this->assertTrue(is_bool($result));
- $this->assertEqual($expected, $result);
-
- $expected = 1;
- $result = $this->db->value('1');
- $this->assertTrue(is_int($result));
- $this->assertEqual($expected, $result);
-
- $expected = 1.1;
- $result = $this->db->value('1.1');
- $this->assertTrue(is_float($result));
- $this->assertEqual($expected, $result);
+ $this->assertIdentical(1, $this->db->value(true));
+ $this->assertIdentical(1, $this->db->value('1'));
+ $this->assertIdentical(1.1, $this->db->value('1.1'));
}
public function testColumnAbstraction() {
$result = $this->db->invokeMethod('_column', array('varchar'));
- $this->assertEqual(array('type' => 'string'), $result);
+ $this->assertIdentical(array('type' => 'string'), $result);
$result = $this->db->invokeMethod('_column', array('tinyint(1)'));
- $this->assertEqual(array('type' => 'boolean'), $result);
+ $this->assertIdentical(array('type' => 'boolean'), $result);
$result = $this->db->invokeMethod('_column', array('varchar(255)'));
- $this->assertEqual(array('type' => 'string', 'length' => '255'), $result);
+ $this->assertIdentical(array('type' => 'string', 'length' => 255), $result);
$result = $this->db->invokeMethod('_column', array('text'));
- $this->assertEqual(array('type' => 'text'), $result);
+ $this->assertIdentical(array('type' => 'text'), $result);
$result = $this->db->invokeMethod('_column', array('text'));
- $this->assertEqual(array('type' => 'text'), $result);
+ $this->assertIdentical(array('type' => 'text'), $result);
$result = $this->db->invokeMethod('_column', array('decimal(12,2)'));
- $this->assertEqual(array('type' => 'float', 'length' => '12,2'), $result);
+ $this->assertIdentical(array('type' => 'float', 'length' => 12, 'precision' => 2), $result);
$result = $this->db->invokeMethod('_column', array('int(11)'));
- $this->assertEqual(array('type' => 'integer', 'length' => '11'), $result);
+ $this->assertIdentical(array('type' => 'integer', 'length' => 11), $result);
}
- public function testAbstractColumnResolution() {
+ public function testRawSqlQuerying() {
+ $this->assertTrue($this->db->create(
+ 'INSERT INTO companies (name, active) VALUES (?, ?)',
+ array('Test', 1)
+ ));
+
+ $result = $this->db->read('SELECT * From companies AS Company WHERE name = {:name}', array(
+ 'name' => 'Test',
+ 'return' => 'array'
+ ));
+ $this->assertEqual(1, count($result));
+ $expected = array('id', 'name', 'active', 'created', 'modified');
+ $this->assertEqual($expected, array_keys($result[0]));
+
+ $this->assertTrue(is_numeric($result[0]['id']));
+ unset($result[0]['id']);
+
+ $expected = array('name' => 'Test', 'active' => '1', 'created' => null, 'modified' => null);
+ $this->assertIdentical($expected, $result[0]);
+
+ $this->assertTrue($this->db->delete('DELETE From companies WHERE name = {:name}', array(
+ 'name' => 'Test'
+ )));
+
+ $result = $this->db->read('SELECT * From companies AS Company WHERE name = {:name}', array(
+ 'name' => 'Test',
+ 'return' => 'array'
+ ));
+ $this->assertFalse($result);
+ }
+ public function testAbstractColumnResolution() {
}
public function testDescribe() {
-
}
-
public function testExecuteException() {
$this->expectException();
$this->db->read('SELECT deliberate syntax error');
}
+
+ public function testEnabledFeatures() {
+ $this->assertTrue(MySql::enabled());
+ $this->assertTrue(MySql::enabled('relationships'));
+ $this->assertFalse(MySql::enabled('arrays'));
+ }
+
+ public function testEntityQuerying() {
+ $sources = $this->db->entities();
+ $this->assertTrue(is_array($sources));
+ $this->assertFalse(empty($sources));
+ }
+
+ public function testQueryOrdering() {
+ $insert = new Query(array(
+ 'type' => 'create',
+ 'source' => 'companies',
+ 'data' => array(
+ 'name' => 'Foo',
+ 'active' => true,
+ 'created' => date('Y-m-d H:i:s')
+ )
+ ));
+ $this->assertIdentical(true, $this->db->create($insert));
+
+ $insert->data(array(
+ 'name' => 'Bar',
+ 'created' => date('Y-m-d H:i:s', strtotime('-5 minutes'))
+ ));
+ $this->assertIdentical(true, $this->db->create($insert));
+
+ $insert->data(array(
+ 'name' => 'Baz',
+ 'created' => date('Y-m-d H:i:s', strtotime('-10 minutes'))
+ ));
+ $this->assertIdentical(true, $this->db->create($insert));
+
+ $read = new Query(array(
+ 'type' => 'read',
+ 'source' => 'companies',
+ 'fields' => array('name'),
+ 'order' => array('created' => 'asc')
+ ));
+ $result = $this->db->read($read, array('return' => 'array'));
+ $expected = array(
+ array('name' => 'Baz'),
+ array('name' => 'Bar'),
+ array('name' => 'Foo')
+ );
+ $this->assertEqual($expected, $result);
+
+ $read->order(array('created' => 'desc'));
+ $result = $this->db->read($read, array('return' => 'array'));
+ $expected = array(
+ array('name' => 'Foo'),
+ array('name' => 'Bar'),
+ array('name' => 'Baz')
+ );
+ $this->assertEqual($expected, $result);
+
+ $delete = new Query(array('type' => 'delete', 'source' => 'companies'));
+ $this->assertTrue($this->db->delete($delete));
+ }
+
+ /**
+ * Ensures that DELETE queries are not generated with table aliases, as MySQL does not support
+ * this.
+ *
+ * @return void
+ */
+ public function testDeletesWithoutAliases() {
+ $delete = new Query(array('type' => 'delete', 'source' => 'companies'));
+ $this->assertTrue($this->db->delete($delete));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/source/database/adapter/Sqlite3Test.php b/libraries/lithium/tests/cases/data/source/database/adapter/Sqlite3Test.php
index 555fa9e..07d8c06 100644
--- a/libraries/lithium/tests/cases/data/source/database/adapter/Sqlite3Test.php
+++ b/libraries/lithium/tests/cases/data/source/database/adapter/Sqlite3Test.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data\source\database\adapter;
-use \lithium\data\Connections;
-use \lithium\data\source\database\adapter\Sqlite3;
-use \lithium\tests\mocks\data\source\database\adapter\MockSqlite3;
+use lithium\data\Connections;
+use lithium\data\source\database\adapter\Sqlite3;
+use lithium\tests\mocks\data\source\database\adapter\MockSqlite3;
class Sqlite3Test extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/data/source/http/adapter/CouchDbTest.php b/libraries/lithium/tests/cases/data/source/http/adapter/CouchDbTest.php
index ebbbc04..11d3b5b 100644
--- a/libraries/lithium/tests/cases/data/source/http/adapter/CouchDbTest.php
+++ b/libraries/lithium/tests/cases/data/source/http/adapter/CouchDbTest.php
@@ -2,25 +2,25 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\data\source\http\adapter;
-use \lithium\data\source\http\adapter\CouchDb;
-
-use \lithium\data\Model;
-use \lithium\data\model\Query;
-use \lithium\data\collection\Document;
+use lithium\data\Model;
+use lithium\data\model\Query;
+use lithium\data\Connections;
+use lithium\data\entity\Document;
+use lithium\data\source\http\adapter\CouchDb;
class CouchDbTest extends \lithium\test\Unit {
+ public $db;
+
+ protected $_configs = array();
+
protected $_testConfig = array(
- 'classes' => array(
- 'service' => '\lithium\tests\mocks\data\source\http\adapter\MockService',
- 'socket' => '\lithium\tests\mocks\data\source\http\adapter\MockSocket'
- ),
'database' => 'lithium-test',
'persistent' => false,
'protocol' => 'tcp',
@@ -28,32 +28,44 @@ class CouchDbTest extends \lithium\test\Unit {
'login' => 'root',
'password' => '',
'port' => 80,
- 'timeout' => 2
+ 'timeout' => 2,
+ 'socket' => 'lithium\tests\mocks\data\source\http\adapter\MockSocket'
);
+ protected $_model = 'lithium\tests\mocks\data\source\http\adapter\MockCouchPost';
+
public function setUp() {
- $model = '\lithium\tests\mocks\data\source\http\adapter\MockCouchPost';
- $this->query = new Query(compact('model') + array(
- 'record' => new Document(compact('model'))
+ $this->_configs = Connections::config();
+
+ Connections::reset();
+ $this->db = new CouchDb(array('socket' => false));
+
+ Connections::config(array(
+ 'mock-couchdb-connection' => array('object' => &$this->db, 'adapter' => 'CouchDb')
));
+
+ $model = $this->_model;
+ $entity = new Document(compact('model'));
+ $this->query = new Query(compact('model', 'entity'));
}
public function tearDown() {
+ Connections::reset();
+ Connections::config($this->_configs);
unset($this->query);
}
public function testAllMethodsNoConnection() {
- $couchdb = new CouchDb(array('classes' => array('socket' => false)));
- $this->assertFalse($couchdb->connect());
- $this->assertTrue($couchdb->disconnect());
- $this->assertFalse($couchdb->get());
- $this->assertFalse($couchdb->post());
- $this->assertFalse($couchdb->put());
+ $this->assertTrue($this->db->connect());
+ $this->assertTrue($this->db->disconnect());
+ $this->assertFalse($this->db->get());
+ $this->assertFalse($this->db->post());
+ $this->assertFalse($this->db->put());
}
public function testConnect() {
- $couchdb = new CouchDb($this->_testConfig);
- $result = $couchdb->connect();
+ $this->db = new CouchDb($this->_testConfig);
+ $result = $this->db->connect();
$this->assertTrue($result);
}
@@ -74,7 +86,9 @@ class CouchDbTest extends \lithium\test\Unit {
public function testDescribe() {
$couchdb = new CouchDb($this->_testConfig);
+ $this->expectException('/companies is not available/');
$result = $couchdb->describe('companies');
+ $this->assertNull($result);
}
public function testItem() {
@@ -82,7 +96,7 @@ class CouchDbTest extends \lithium\test\Unit {
$data = array(
'_id' => 'a1', '_rev' => '1-2', 'author' => 'author 1', 'body' => 'body 1'
);
- $expected =array(
+ $expected = array(
'id' => 'a1', 'rev' => '1-2',
'author' => 'author 1', 'body' => 'body 1'
);
@@ -94,9 +108,9 @@ class CouchDbTest extends \lithium\test\Unit {
public function testCreateNoId() {
$couchdb = new CouchDb($this->_testConfig);
$this->query->data(array('name' => 'Acme Inc.'));
- $expected = true;
+
$result = $couchdb->create($this->query);
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
$expected = '/lithium-test';
$result = $couchdb->last->request->path;
@@ -110,9 +124,9 @@ class CouchDbTest extends \lithium\test\Unit {
public function testCreateWithId() {
$couchdb = new CouchDb($this->_testConfig);
$this->query->data(array('id' => 12345, 'name' => 'Acme Inc.'));
- $expected = true;
+
$result = $couchdb->create($this->query);
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
$expected = '/lithium-test/12345';
$result = $couchdb->last->request->path;
@@ -125,8 +139,10 @@ class CouchDbTest extends \lithium\test\Unit {
public function testReadNoConditions() {
$couchdb = new CouchDb($this->_testConfig);
- $expected = true; $result = $couchdb->read($this->query);
- $this->assertEqual($expected, $result);
+
+ $result = $couchdb->read($this->query);
+ $this->assertTrue($result);
+ $this->assertEqual(array('total_rows' => null, 'offset' => null), $result->stats());
$expected = '/lithium-test/_all_docs';
$result = $couchdb->last->request->path;
@@ -139,10 +155,11 @@ class CouchDbTest extends \lithium\test\Unit {
public function testReadWithConditions() {
$couchdb = new CouchDb($this->_testConfig);
- $expected = true;
+
$this->query->conditions(array('id' => 12345));
$result = $couchdb->read($this->query);
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
+ $this->assertEqual(array('total_rows' => null, 'offset' => null), $result->stats());
$expected = '/lithium-test/12345';
$result = $couchdb->last->request->path;
@@ -151,16 +168,21 @@ class CouchDbTest extends \lithium\test\Unit {
$expected = '';
$result = $couchdb->last->request->params;
$this->assertEqual($expected, $result);
+
+ $this->query->conditions(array('id' => 12345, 'path' => '/lithium-test/12345'));
+ $result = $couchdb->read($this->query);
+ $this->assertTrue($result);
}
public function testReadWithViewConditions() {
$couchdb = new CouchDb($this->_testConfig);
- $expected = true;
+
$this->query->conditions(array(
'design' => 'latest', 'view' => 'all', 'limit' => 10, 'descending' => 'true'
));
$result = $couchdb->read($this->query);
- $this->assertEqual($expected, $result);
+ $this->assertEqual(array('total_rows' => null, 'offset' => null), $result->stats());
+ $this->assertTrue($result);
$expected = '/lithium-test/_design/latest/_view/all/';
$result = $couchdb->last->request->path;
@@ -280,18 +302,16 @@ class CouchDbTest extends \lithium\test\Unit {
public function testResultClose() {
$couchdb = new CouchDb($this->_testConfig);
- $expected = null;
$result = $couchdb->result('close', (object) array(), $this->query);
- $this->assertEqual($expected, $result);
+ $this->assertNull($result);
}
public function testUpdate() {
$couchdb = new CouchDb($this->_testConfig);
$this->query->data(array('id' => 12345, 'rev' => '1-1', 'title' => 'One'));
- $expected = true;
$result = $couchdb->update($this->query);
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
$expected = '/lithium-test/12345';
$result = $couchdb->last->request->path;
@@ -306,9 +326,8 @@ class CouchDbTest extends \lithium\test\Unit {
$couchdb = new CouchDb($this->_testConfig);
$this->query->data(array('id' => 12345, 'rev'=> '1-1', 'name' => 'Acme Inc'));
- $expected = true;
$result = $couchdb->delete($this->query);
- $this->assertEqual($expected, $result);
+ $this->assertTrue($result);
$expected = '/lithium-test/12345';
$result = $couchdb->last->request->path;
@@ -318,6 +337,15 @@ class CouchDbTest extends \lithium\test\Unit {
$result = $couchdb->last->request->params;
$this->assertEqual($expected, $result);
}
+
+ public function testEnabled() {
+ $this->assertEqual(CouchDb::enabled(), true);
+
+ $this->assertEqual(CouchDb::enabled('arrays'), true);
+ $this->assertEqual(CouchDb::enabled('transactions'), false);
+ $this->assertEqual(CouchDb::enabled('booleans'), true);
+ $this->assertEqual(CouchDb::enabled('relationships'), false);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/source/mongo_db/ExporterTest.php b/libraries/lithium/tests/cases/data/source/mongo_db/ExporterTest.php
new file mode 100644
index 0000000..e327305
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/source/mongo_db/ExporterTest.php
@@ -0,0 +1,321 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\data\source\mongo_db;
+
+use MongoId;
+use MongoDate;
+use lithium\data\source\MongoDb;
+use lithium\data\entity\Document;
+use lithium\data\collection\DocumentArray;
+use lithium\data\source\mongo_db\Exporter;
+
+class ExporterTest extends \lithium\test\Unit {
+
+ protected $_model = 'lithium\tests\mocks\data\source\MockMongoPost';
+
+ protected $_schema = array(
+ '_id' => array('type' => 'id'),
+ 'guid' => array('type' => 'id'),
+ 'title' => array('type' => 'string'),
+ 'tags' => array('type' => 'string', 'array' => true),
+ 'comments' => array('type' => 'MongoId'),
+ 'authors' => array('type' => 'MongoId', 'array' => true),
+ 'created' => array('type' => 'MongoDate'),
+ 'modified' => array('type' => 'datetime'),
+ 'voters' => array('type' => 'id', 'array' => true),
+ 'rank_count' => array('type' => 'integer', 'default' => 0),
+ 'rank' => array('type' => 'float', 'default' => 0.0),
+ 'notifications.foo' => array('type' => 'boolean'),
+ 'notifications.bar' => array('type' => 'boolean'),
+ 'notifications.baz' => array('type' => 'boolean'),
+ );
+
+ protected $_handlers = array();
+
+ public function skip() {
+ $this->skipIf(!MongoDb::enabled(), 'MongoDb is not enabled');
+ }
+
+ public function setUp() {
+ $this->_handlers = array(
+ 'id' => function($v) {
+ return is_string($v) && preg_match('/^[0-9a-f]{24}$/', $v) ? new MongoId($v) : $v;
+ },
+ 'date' => function($v) {
+ $v = is_numeric($v) ? intval($v) : strtotime($v);
+ return (time() == $v) ? new MongoDate() : new MongoDate($v);
+ },
+ 'regex' => function($v) { return new MongoRegex($v); },
+ 'integer' => function($v) { return (integer) $v; },
+ 'float' => function($v) { return (float) $v; },
+ 'boolean' => function($v) { return (boolean) $v; },
+ 'code' => function($v) { return new MongoCode($v); },
+ 'binary' => function($v) { return new MongoBinData($v); },
+ );
+ $model = $this->_model;
+ $model::resetConnection(true);
+ }
+
+ public function testInvalid() {
+ $this->assertNull(Exporter::get(null, null));
+ }
+
+ public function testCreateWithFixedData() {
+ $doc = new Document(array('exists' => false, 'data' => array(
+ '_id' => new MongoId(),
+ 'created' => new MongoDate(),
+ 'numbers' => new DocumentArray(array('data' => array(7, 8, 9))),
+ 'objects' => new DocumentArray(array('data' => array(
+ new Document(array('data' => array('foo' => 'bar'))),
+ new Document(array('data' => array('baz' => 'dib')))
+ ))),
+ 'deeply' => new Document(array('data' => array('nested' => 'object')))
+ )));
+ $this->assertEqual('object', $doc->deeply->nested);
+ $this->assertTrue($doc->_id instanceof MongoId);
+
+ $result = Exporter::get('create', $doc->export());
+ $this->assertTrue($result['create']['_id'] instanceof MongoId);
+ $this->assertTrue($result['create']['created'] instanceof MongoDate);
+ $this->assertIdentical(time(), $result['create']['created']->sec);
+
+ $this->assertIdentical(array(7, 8, 9), $result['create']['numbers']);
+ $expected = array(array('foo' => 'bar'), array('baz' => 'dib'));
+ $this->assertIdentical($expected, $result['create']['objects']);
+ $this->assertIdentical(array('nested' => 'object'), $result['create']['deeply']);
+ }
+
+ public function testCreateWithChangedData() {
+ $doc = new Document(array('exists' => false, 'data' => array(
+ 'numbers' => new DocumentArray(array('data' => array(7, 8, 9))),
+ 'objects' => new DocumentArray(array('data' => array(
+ new Document(array('data' => array('foo' => 'bar'))),
+ new Document(array('data' => array('baz' => 'dib')))
+ ))),
+ 'deeply' => new Document(array('data' => array('nested' => 'object')))
+ )));
+ $doc->numbers[] = 10;
+ $doc->deeply->nested2 = 'object2';
+ $doc->objects[1]->dib = 'gir';
+
+ $expected = array(
+ 'numbers' => array(7, 8, 9, 10),
+ 'objects' => array(array('foo' => 'bar'), array('baz' => 'dib', 'dib' => 'gir')),
+ 'deeply' => array('nested' => 'object', 'nested2' => 'object2')
+ );
+ $result = Exporter::get('create', $doc->export());
+ $this->assertEqual(array('create'), array_keys($result));
+ $this->assertEqual($expected, $result['create']);
+ }
+
+ public function testUpdateWithNoChanges() {
+ $doc = new Document(array('exists' => true, 'data' => array(
+ 'numbers' => new DocumentArray(array('exists' => true, 'data' => array(7, 8, 9))),
+ 'objects' => new DocumentArray(array('exists' => true, 'data' => array(
+ new Document(array('exists' => true, 'data' => array('foo' => 'bar'))),
+ new Document(array('exists' => true, 'data' => array('baz' => 'dib')))
+ ))),
+ 'deeply' => new Document(array('exists' => true, 'data' => array('nested' => 'object')))
+ )));
+ $this->assertFalse(Exporter::get('update', $doc->export()));
+ }
+
+ public function testUpdateWithSubObjects() {
+ $doc = new Document(array('exists' => true, 'data' => array(
+ 'numbers' => new DocumentArray(array('data' => array(7, 8, 9))),
+ 'deeply' => new Document(array(
+ 'pathKey' => 'deeply', 'exists' => true, 'data' => array('nested' => 'object')
+ )),
+ 'foo' => 'bar'
+ )));
+ $doc->field = 'value';
+ $doc->deeply->nested = 'foo';
+ $doc->newObject = new Document(array(
+ 'exists' => false, 'data' => array('subField' => 'subValue')
+ ));
+
+ $this->assertEqual('foo', $doc->deeply->nested);
+ $this->assertEqual('subValue', $doc->newObject->subField);
+
+ $result = Exporter::get('update', $doc->export());
+ $this->assertFalse(isset($result['update']['foo']));
+ $this->assertEqual('value', $result['update']['field']);
+ $this->assertEqual(array('subField' => 'subValue'), $result['update']['newObject']);
+ $this->assertEqual('foo', $result['update']['deeply.nested']);
+ }
+
+ public function testFieldRemoval() {
+ $doc = new Document(array('exists' => true, 'data' => array(
+ 'numbers' => new DocumentArray(array('data' => array(7, 8, 9))),
+ 'deeply' => new Document(array(
+ 'pathKey' => 'deeply', 'exists' => true, 'data' => array('nested' => 'object')
+ )),
+ 'foo' => 'bar'
+ )));
+ $doc->set(array('flagged' => true, 'foo' => 'baz', 'bar' => 'dib'));
+ unset($doc->foo, $doc->flagged, $doc->numbers, $doc->deeply->nested);
+
+ $result = Exporter::get('update', $doc->export());
+ $expected = array(
+ 'foo' => true, 'flagged' => true, 'numbers' => true, 'deeply.nested' => true
+ );
+ $this->assertEqual($expected, $result['remove']);
+ $this->assertEqual(array('bar' => 'dib'), $result['update']);
+ }
+
+ /**
+ * Tests that when an existing object is attached as a value of another existing object, the
+ * whole sub-object is re-written to the new value.
+ *
+ * @return void
+ */
+ public function testAppendExistingObjects() {
+ $doc = new Document(array('exists' => true, 'data' => array(
+ 'deeply' => new Document(array(
+ 'pathKey' => 'deeply', 'exists' => true, 'data' => array('nested' => 'object')
+ )),
+ 'foo' => 'bar'
+ )));
+ $append = new Document(array('exists' => true, 'data' => array('foo' => 'bar')));
+
+ $doc->deeply = $append;
+ $result = Exporter::get('update', $doc->export());
+ $expected = array('update' => array('deeply' => array('foo' => 'bar')));
+ $this->assertEqual($expected, $result);
+ $doc->update();
+
+ $expected = array('$set' => array('deeply' => array('foo' => 'bar')));
+ $this->assertEqual($expected, Exporter::toCommand($result));
+
+ $doc->append2 = new Document(array('exists' => false, 'data' => array('foo' => 'bar')));
+ $expected = array('update' => array('append2' => array('foo' => 'bar')));
+ $this->assertEqual($expected, Exporter::get('update', $doc->export()));
+ $doc->update();
+
+ $this->assertFalse(Exporter::get('update', $doc->export()));
+ $doc->append2->foo = 'baz';
+ $doc->append2->bar = 'dib';
+ $doc->deeply->nested = true;
+
+ $expected = array('update' => array(
+ 'append2.foo' => 'baz', 'append2.bar' => 'dib', 'deeply.nested' => true
+ ));
+ $this->assertEqual($expected, Exporter::get('update', $doc->export()));
+ }
+
+ public function testNestedObjectCasting() {
+ $model = $this->_model;
+ $data = array('notifications' => array('foo' => '', 'bar' => '1', 'baz' => 0, 'dib' => 42));
+
+ $model::schema($this->_schema);
+ $result = Exporter::cast($data, $this->_schema, $model::connection(), compact('model'));
+
+ $this->assertIdentical(false, $result['notifications']->foo);
+ $this->assertIdentical(true, $result['notifications']->bar);
+ $this->assertIdentical(false, $result['notifications']->baz);
+ $this->assertIdentical(42, $result['notifications']->dib);
+ }
+
+ /**
+ * Tests handling type values based on specified schema settings.
+ *
+ * @return void
+ */
+ public function testTypeCasting() {
+ $data = array(
+ '_id' => '4c8f86167675abfabd970300',
+ 'title' => 'Foo',
+ 'tags' => 'test',
+ 'comments' => array(
+ "4c8f86167675abfabdbe0300", "4c8f86167675abfabdbf0300", "4c8f86167675abfabdc00300"
+ ),
+ 'authors' => '4c8f86167675abfabdb00300',
+ 'created' => time(),
+ 'modified' => date('Y-m-d H:i:s'),
+ 'rank_count' => '45',
+ 'rank' => '3.45688'
+ );
+ $time = time();
+ $model = $this->_model;
+ $handlers = $this->_handlers;
+ $options = compact('model', 'handlers');
+ $result = Exporter::cast($data, $this->_schema, $model::connection(), $options);
+
+ $this->assertEqual(array_keys($data), array_keys($result));
+ $this->assertTrue($result['_id'] instanceof MongoId);
+ $this->assertEqual('4c8f86167675abfabd970300', (string) $result['_id']);
+
+ $this->assertTrue($result['comments'] instanceof DocumentArray);
+ $this->assertEqual(3, count($result['comments']));
+
+ $this->assertTrue($result['comments'][0] instanceof MongoId);
+ $this->assertTrue($result['comments'][1] instanceof MongoId);
+ $this->assertTrue($result['comments'][2] instanceof MongoId);
+ $this->assertEqual('4c8f86167675abfabdbe0300', (string) $result['comments'][0]);
+ $this->assertEqual('4c8f86167675abfabdbf0300', (string) $result['comments'][1]);
+ $this->assertEqual('4c8f86167675abfabdc00300', (string) $result['comments'][2]);
+
+ $this->assertEqual($data['comments'], $result['comments']->data());
+ $this->assertEqual(array('test'), $result['tags']->data());
+ $this->assertEqual(array('4c8f86167675abfabdb00300'), $result['authors']->data());
+ $this->assertTrue($result['authors'][0] instanceof MongoId);
+
+ $this->assertTrue($result['modified'] instanceof MongoDate);
+ $this->assertTrue($result['created'] instanceof MongoDate);
+ $this->assertTrue($result['created']->usec > 0);
+
+ $this->assertEqual($time, $result['modified']->sec);
+ $this->assertEqual($time, $result['created']->sec);
+
+ $this->assertIdentical(45, $result['rank_count']);
+ $this->assertIdentical(3.45688, $result['rank']);
+ }
+
+ public function testWithArraySchema() {
+ $model = $this->_model;
+ $model::schema(array(
+ '_id' => array('type' => 'id'),
+ 'list' => array('array' => true),
+ 'list.foo' => array('type' => 'string'),
+ 'list.bar' => array('type' => 'string')
+ ));
+ $doc = new Document(compact('model'));
+ $doc->list[] = array('foo' => '!!', 'bar' => '??');
+
+ $data = array('list' => array(array('foo' => '!!', 'bar' => '??')));
+ $this->assertEqual($data, $doc->data());
+
+ $result = Exporter::get('create', $doc->export());
+ $this->assertEqual($data, $result['create']);
+
+ $result = Exporter::get('update', $doc->export());
+ $this->assertEqual($data, $result['update']);
+
+ $doc = new Document(compact('model'));
+ $doc->list = array();
+ $doc->list[] = array('foo' => '!!', 'bar' => '??');
+
+ $data = array('list' => array(array('foo' => '!!', 'bar' => '??')));
+ $this->assertEqual($data, $doc->data());
+
+ $result = Exporter::get('create', $doc->export());
+ $this->assertEqual($result['create'], $data);
+
+ $result = Exporter::get('update', $doc->export());
+ $this->assertEqual($result['update'], $data);
+ }
+
+ /**
+ * @todo Implement me.
+ */
+ public function testCreateWithWhitelist() {
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/CatalogTest.php b/libraries/lithium/tests/cases/g11n/CatalogTest.php
index 0611051..af27975 100644
--- a/libraries/lithium/tests/cases/g11n/CatalogTest.php
+++ b/libraries/lithium/tests/cases/g11n/CatalogTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\g11n;
-use \lithium\g11n\Catalog;
-use \lithium\g11n\catalog\adapter\Memory;
+use lithium\g11n\Catalog;
+use lithium\g11n\catalog\adapter\Memory;
class CatalogTest extends \lithium\test\Unit {
@@ -34,19 +34,32 @@ class CatalogTest extends \lithium\test\Unit {
* @return void
*/
public function testRead() {
- $result = Catalog::read('validation.ssn', 'de_DE');
+ $result = Catalog::read('runtime', 'validation.ssn', 'de_DE');
$this->assertNull($result);
}
/**
+ * Tests for values returned by `write()`.
+ *
+ * @return void
+ */
+ public function testWrite() {
+ $data = array(
+ 'DKK' => 'Dänische Krone'
+ );
+ $result = Catalog::write('runtime', 'currency', 'de_DE', $data);
+ $this->assertTrue($result);
+ }
+
+ /**
* Tests writing and reading for single and multiple items.
*
* @return void
*/
public function testWriteRead() {
$data = '/postalCode en_US/';
- Catalog::write('validation.postalCode', 'en_US', $data, array('name' => 'runtime'));
- $result = Catalog::read('validation.postalCode', 'en_US');
+ Catalog::write('runtime', 'validation.postalCode', 'en_US', $data);
+ $result = Catalog::read('runtime', 'validation.postalCode', 'en_US');
$this->assertEqual($data, $result);
$this->tearDown();
@@ -56,8 +69,8 @@ class CatalogTest extends \lithium\test\Unit {
'GRD' => 'Griechische Drachme',
'DKK' => 'Dänische Krone'
);
- Catalog::write('currency', 'de', $data, array('name' => 'runtime'));
- $result = Catalog::read('currency', 'de');
+ Catalog::write('runtime', 'currency', 'de', $data);
+ $result = Catalog::read('runtime', 'currency', 'de');
$this->assertEqual($data, $result);
}
@@ -71,8 +84,8 @@ class CatalogTest extends \lithium\test\Unit {
*/
public function testWriteReadMergeLocales() {
$data = '/postalCode en/';
- Catalog::write('validation.postalCode', 'en', $data, array('name' => 'runtime'));
- $result = Catalog::read('validation.postalCode', 'en_US');
+ Catalog::write('runtime', 'validation.postalCode', 'en', $data);
+ $result = Catalog::read('runtime', 'validation.postalCode', 'en_US');
$expected = '/postalCode en/';
$this->assertEqual($expected, $result);
@@ -80,10 +93,10 @@ class CatalogTest extends \lithium\test\Unit {
$this->setUp();
$data = '/postalCode en_US/';
- Catalog::write('validation.postalCode', 'en_US', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'validation.postalCode', 'en_US', $data);
$data = '/postalCode en/';
- Catalog::write('validation.postalCode', 'en', $data, array('name' => 'runtime'));
- $result = Catalog::read('validation.postalCode', 'en_US');
+ Catalog::write('runtime', 'validation.postalCode', 'en', $data);
+ $result = Catalog::read('runtime', 'validation.postalCode', 'en_US');
$expected = '/postalCode en_US/';
$this->assertEqual($expected, $result);
@@ -91,8 +104,8 @@ class CatalogTest extends \lithium\test\Unit {
$this->setUp();
$data = array('a' => true, 'b' => true, 'c' => true);
- Catalog::write('language', 'en', $data, array('name' => 'runtime'));
- $result = Catalog::read('language', 'en_US');
+ Catalog::write('runtime', 'language', 'en', $data);
+ $result = Catalog::read('runtime', 'language', 'en_US');
$expected = array('a' => true, 'b' => true, 'c' => true);
$this->assertEqual($expected, $result);
@@ -102,12 +115,12 @@ class CatalogTest extends \lithium\test\Unit {
$data = array(
'DKK' => 'Dänische Krone'
);
- Catalog::write('currency', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'currency', 'de', $data);
$data = array(
'GRD' => 'Griechische Drachme',
);
- Catalog::write('currency', 'de_CH', $data, array('name' => 'runtime'));
- $result = Catalog::read('currency', 'de_CH');
+ Catalog::write('runtime', 'currency', 'de_CH', $data);
+ $result = Catalog::read('runtime', 'currency', 'de_CH');
$expected = array(
'GRD' => 'Griechische Drachme',
'DKK' => 'Dänische Krone'
@@ -121,12 +134,12 @@ class CatalogTest extends \lithium\test\Unit {
'GRD' => 'de Griechische Drachme',
'DKK' => 'de Dänische Krone'
);
- Catalog::write('currency', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'currency', 'de', $data);
$data = array(
'GRD' => 'de_CH Griechische Drachme',
);
- Catalog::write('currency', 'de_CH', $data, array('name' => 'runtime'));
- $result = Catalog::read('currency', 'de_CH');
+ Catalog::write('runtime', 'currency', 'de_CH', $data);
+ $result = Catalog::read('runtime', 'currency', 'de_CH');
$expected = array(
'GRD' => 'de_CH Griechische Drachme',
'DKK' => 'de Dänische Krone'
@@ -141,43 +154,43 @@ class CatalogTest extends \lithium\test\Unit {
*/
public function testWriteReadWithScope() {
$data = '/postalCode en_US scope0/';
- Catalog::write('validation.postalCode', 'en_US', $data, array(
- 'name' => 'runtime',
+ Catalog::write('runtime', 'validation.postalCode', 'en_US', $data, array(
'scope' => 'scope0'
));
$data = '/postalCode en_US scope1/';
- Catalog::write('validation.postalCode', 'en_US', $data, array(
- 'name' => 'runtime',
+ Catalog::write('runtime', 'validation.postalCode', 'en_US', $data, array(
'scope' => 'scope1'
));
- $result = Catalog::read('validation.postalCode', 'en_US');
+ $result = Catalog::read('runtime', 'validation.postalCode', 'en_US');
$this->assertNull($result);
- $result = Catalog::read('validation.postalCode', 'en_US', array('scope' => 'scope0'));
+ $result = Catalog::read('runtime', 'validation.postalCode', 'en_US', array(
+ 'scope' => 'scope0'
+ ));
$expected = '/postalCode en_US scope0/';
$this->assertEqual($expected, $result);
- $result = Catalog::read('validation.postalCode', 'en_US', array('scope' => 'scope1'));
+ $result = Catalog::read('runtime', 'validation.postalCode', 'en_US', array(
+ 'scope' => 'scope1'
+ ));
$expected = '/postalCode en_US scope1/';
$this->assertEqual($expected, $result);
$data = '/postalCode en_US/';
- Catalog::write('validation.postalCode', 'en_US', $data, array(
- 'name' => 'runtime'
- ));
+ Catalog::write('runtime', 'validation.postalCode', 'en_US', $data);
- $result = Catalog::read('validation.postalCode', 'en_US');
+ $result = Catalog::read('runtime', 'validation.postalCode', 'en_US');
$expected = '/postalCode en_US/';
$this->assertEqual($expected, $result);
}
/**
- * Tests reading from multiple configured stores with fallbacks.
+ * Tests reading from all configured stores with fallbacks.
*
* @return void
*/
- public function testWriteReadMergeConfigurations() {
+ public function testWriteReadMergeAllConfigurations() {
Catalog::reset();
Catalog::config(array(
'runtime0' => array('adapter' => new Memory()),
@@ -185,12 +198,12 @@ class CatalogTest extends \lithium\test\Unit {
));
$data = '/postalCode en0/';
- Catalog::write('validation.postalCode', 'en', $data, array('name' => 'runtime0'));
+ Catalog::write('runtime0', 'validation.postalCode', 'en', $data);
$data = '/postalCode en_US1/';
- Catalog::write('validation.postalCode', 'en_US', $data, array('name' => 'runtime1'));
+ Catalog::write('runtime1', 'validation.postalCode', 'en_US', $data);
$data = '/postalCode en1/';
- Catalog::write('validation.postalCode', 'en', $data, array('name' => 'runtime1'));
- $result = Catalog::read('validation.postalCode', 'en_US');
+ Catalog::write('runtime1', 'validation.postalCode', 'en', $data);
+ $result = Catalog::read(true, 'validation.postalCode', 'en_US');
$expected = '/postalCode en_US1/';
$this->assertEqual($expected, $result);
@@ -204,16 +217,16 @@ class CatalogTest extends \lithium\test\Unit {
'GRD' => 'de0 Griechische Drachme',
'DKK' => 'de0 Dänische Krone'
);
- Catalog::write('currency', 'de', $data, array('name' => 'runtime0'));
+ Catalog::write('runtime0', 'currency', 'de', $data);
$data = array(
'GRD' => 'de1 Griechische Drachme'
);
- Catalog::write('currency', 'de', $data, array('name' => 'runtime1'));
+ Catalog::write('runtime1', 'currency', 'de', $data);
$data = array(
'GRD' => 'de_CH1 Griechische Drachme'
);
- Catalog::write('currency', 'de_CH', $data, array('name' => 'runtime1'));
- $result = Catalog::read('currency', 'de_CH');
+ Catalog::write('runtime1', 'currency', 'de_CH', $data);
+ $result = Catalog::read(true, 'currency', 'de_CH');
$expected = array(
'GRD' => 'de_CH1 Griechische Drachme',
'DKK' => 'de0 Dänische Krone'
@@ -222,19 +235,65 @@ class CatalogTest extends \lithium\test\Unit {
}
/**
+ * Tests reading from selected multiple configured stores.
+ *
+ * @return void
+ */
+ public function testReadMergeSelectedConfigurations() {
+ Catalog::reset();
+ Catalog::config(array(
+ 'runtime0' => array('adapter' => new Memory()),
+ 'runtime1' => array('adapter' => new Memory()),
+ 'runtime2' => array('adapter' => new Memory())
+ ));
+
+ $data = '/postalCode en0/';
+ Catalog::write('runtime0', 'validation.postalCode', 'en', $data);
+ $data = '/postalCode en1/';
+ Catalog::write('runtime1', 'validation.postalCode', 'en', $data);
+ $data = '/postalCode en2/';
+ Catalog::write('runtime2', 'validation.postalCode', 'en', $data);
+ $data = '/ssn en2/';
+ Catalog::write('runtime2', 'validation.ssn', 'en', $data);
+
+ $result = Catalog::read('runtime0', 'validation.postalCode', 'en');
+ $expected = '/postalCode en0/';
+ $this->assertEqual($expected, $result);
+
+ $result = Catalog::read('runtime2', 'validation.postalCode', 'en');
+ $expected = '/postalCode en2/';
+ $this->assertEqual($expected, $result);
+
+ $result = Catalog::read('runtime2', 'validation.postalCode', 'en');
+ $expected = '/postalCode en2/';
+ $this->assertEqual($expected, $result);
+
+ $result = Catalog::read(array('runtime0', 'runtime2'), 'validation', 'en');
+ $expected = array(
+ 'postalCode' => '/postalCode en0/',
+ 'ssn' => '/ssn en2/',
+ );
+ $this->assertEqual($expected, $result);
+
+ $resultA = Catalog::read(array('runtime0', 'runtime2'), 'validation', 'en');
+ $resultB = Catalog::read(true, 'validation', 'en');
+ $this->assertEqual($resultA, $resultB);
+ }
+
+ /**
* Tests writing, then reading different types of values.
*
* @return void
*/
public function testDataTypeSupport() {
$data = function($n) { return $n == 1 ? 0 : 1; };
- Catalog::write('message.plural', 'en', $data, array('name' => 'runtime'));
- $result = Catalog::read('message.plural', 'en');
+ Catalog::write('runtime', 'message.pluralRule', 'en', $data);
+ $result = Catalog::read('runtime', 'message.pluralRule', 'en');
$this->assertEqual($data, $result);
$data = array('fish', 'fishes');
- Catalog::write('message.fish', 'en', $data, array('name' => 'runtime'));
- $result = Catalog::read('message.fish', 'en');
+ Catalog::write('runtime', 'message.fish', 'en', $data);
+ $result = Catalog::read('runtime', 'message.fish', 'en');
$this->assertEqual($data, $result);
}
@@ -245,83 +304,82 @@ class CatalogTest extends \lithium\test\Unit {
*/
public function testInputFormatNormalization() {
$data = array('house' => 'Haus');
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
- $result = Catalog::read('message', 'de', array('lossy' => false));
- $expected = array(
- 'house' => array(
- 'id' => 'house',
- 'ids' => array(),
- 'translated' => 'Haus',
- 'flags' => array(),
- 'comments' => array(),
- 'occurrences' => array()
+ Catalog::write('runtime', 'message', 'de', $data);
+ $result = Catalog::read('runtime', 'message', 'de', array('lossy' => false));
+ $expected = array('house' => array(
+ 'id' => 'house',
+ 'ids' => array(),
+ 'translated' => 'Haus',
+ 'flags' => array(),
+ 'comments' => array(),
+ 'occurrences' => array()
));
$this->assertEqual($expected, $result);
- $data = array(
- 'house' => array(
- 'id' => 'house',
- 'ids' => array(),
- 'translated' => 'Haus',
- 'flags' => array(),
- 'comments' => array(),
- 'occurrences' => array()
+ $data = array('house' => array(
+ 'id' => 'house',
+ 'ids' => array(),
+ 'translated' => 'Haus',
+ 'flags' => array(),
+ 'comments' => array(),
+ 'occurrences' => array()
));
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
- $result = Catalog::read('message', 'de', array('lossy' => false));
- $expected = array(
- 'house' => array(
- 'id' => 'house',
- 'ids' => array(),
- 'translated' => 'Haus',
- 'flags' => array(),
- 'comments' => array(),
- 'occurrences' => array()
+ Catalog::write('runtime', 'message', 'de', $data);
+ $result = Catalog::read('runtime', 'message', 'de', array('lossy' => false));
+ $expected = array('house' => array(
+ 'id' => 'house',
+ 'ids' => array(),
+ 'translated' => 'Haus',
+ 'flags' => array(),
+ 'comments' => array(),
+ 'occurrences' => array()
));
$this->assertEqual($expected, $result);
}
public function testOutputLossyFormat() {
- $data = array(
- 'house' => array(
- 'id' => 'house',
- 'ids' => array('singular' => 'house'),
- 'translated' => 'Haus',
- 'flags' => array(),
- 'comments' => array(),
- 'occurrences' => array()
+ $data = array('house' => array(
+ 'id' => 'house',
+ 'ids' => array('singular' => 'house'),
+ 'translated' => 'Haus',
+ 'flags' => array(),
+ 'comments' => array(),
+ 'occurrences' => array()
));
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
- $result = Catalog::read('message', 'de');
- $expected = array(
- 'house' => 'Haus'
- );
+ Catalog::write('runtime', 'message', 'de', $data);
+ $result = Catalog::read('runtime', 'message', 'de');
+ $expected = array('house' => 'Haus');
$this->assertEqual($expected, $result);
}
public function testOutputLosslessFormat() {
- $data = array(
- 'house' => array(
- 'id' => 'house',
- 'ids' => array('singular' => 'house'),
- 'translated' => 'Haus',
- 'flags' => array(),
- 'comments' => array(),
- 'occurrences' => array()
+ $data = array('house' => array(
+ 'id' => 'house',
+ 'ids' => array('singular' => 'house'),
+ 'translated' => 'Haus',
+ 'flags' => array(),
+ 'comments' => array(),
+ 'occurrences' => array()
));
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
- $result = Catalog::read('message', 'de', array('lossy' => false));
- $expected = array(
- 'house' => array(
- 'id' => 'house',
- 'ids' => array('singular' => 'house'),
- 'translated' => 'Haus',
- 'flags' => array(),
- 'comments' => array(),
- 'occurrences' => array()
+ Catalog::write('runtime', 'message', 'de', $data);
+ $result = Catalog::read('runtime', 'message', 'de', array('lossy' => false));
+ $expected = array('house' => array(
+ 'id' => 'house',
+ 'ids' => array('singular' => 'house'),
+ 'translated' => 'Haus',
+ 'flags' => array(),
+ 'comments' => array(),
+ 'occurrences' => array()
));
$this->assertEqual($expected, $result);
}
+
+ public function testInvalidWrite() {
+ Catalog::reset();
+ $data = array('house' => array('id' => 'house'));
+ $this->expectException("Configuration `runtime` has not been defined.");
+ $this->assertFalse(Catalog::write('runtime', 'message', 'de', $data));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/LocaleTest.php b/libraries/lithium/tests/cases/g11n/LocaleTest.php
index b7734e5..f3d0343 100644
--- a/libraries/lithium/tests/cases/g11n/LocaleTest.php
+++ b/libraries/lithium/tests/cases/g11n/LocaleTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\g11n;
-use \lithium\g11n\Locale;
-use \lithium\action\Request as ActionRequest;
-use \lithium\console\Request as ConsoleRequest;
+use lithium\g11n\Locale;
+use lithium\action\Request as ActionRequest;
+use lithium\console\Request as ConsoleRequest;
class LocaleTest extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/g11n/MessageTest.php b/libraries/lithium/tests/cases/g11n/MessageTest.php
index 6d37017..aa6df4f 100644
--- a/libraries/lithium/tests/cases/g11n/MessageTest.php
+++ b/libraries/lithium/tests/cases/g11n/MessageTest.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\g11n;
-use \lithium\core\Environment;
-use \lithium\g11n\Message;
-use \lithium\g11n\Catalog;
-use \lithium\g11n\catalog\adapter\Memory;
+use lithium\core\Environment;
+use lithium\g11n\Message;
+use lithium\g11n\Catalog;
+use lithium\g11n\catalog\adapter\Memory;
class MessageTest extends \lithium\test\Unit {
@@ -24,11 +24,12 @@ class MessageTest extends \lithium\test\Unit {
'runtime' => array('adapter' => new Memory())
));
$data = function($n) { return $n == 1 ? 0 : 1; };
- Catalog::write('message.plural', 'root', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message.pluralRule', 'root', $data);
$this->_backups['environment'] = Environment::get('test');
Environment::set('test', array('locale' => 'en'));
Environment::set('test');
+ Message::cache(false);
}
public function tearDown() {
@@ -39,10 +40,8 @@ class MessageTest extends \lithium\test\Unit {
}
public function testTranslateBasic() {
- $data = array(
- 'catalog' => 'Katalog',
- );
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ $data = array('catalog' => 'Katalog');
+ Catalog::write('runtime', 'message', 'de', $data);
$expected = 'Katalog';
$result = Message::translate('catalog', array('locale' => 'de'));
@@ -53,7 +52,7 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'house' => array('Haus', 'Häuser')
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$expected = 'Haus';
$result = Message::translate('house', array('locale' => 'de'));
@@ -68,7 +67,7 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'house' => array('Haus', 'Häuser')
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$expected = 'Häuser';
$result = Message::translate('house', array('locale' => 'de', 'count' => 2.31));
@@ -103,7 +102,7 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'house' => array('Haus', 'Häuser')
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$expected = 'Haus';
$result = Message::translate('house', array('locale' => 'de', 'count' => -1));
@@ -130,13 +129,13 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'catalog' => array('Katalog', 'Kataloge'),
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$result = Message::translate('catalog', array('locale' => 'de'));
$this->assertNull($result);
$data = 'not a valid pluralization function';
- Catalog::write('message.plural', 'root', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message.pluralRule', 'root', $data);
$result = Message::translate('catalog', array('locale' => 'de'));
$this->assertNull($result);
@@ -146,11 +145,11 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'catalog' => 'Katalog',
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime', 'scope' => 'test'));
+ Catalog::write('runtime', 'message', 'de', $data, array('scope' => 'test'));
$data = function($n) { return $n == 1 ? 0 : 1; };
- Catalog::write('message.plural', 'root', $data, array(
- 'name' => 'runtime', 'scope' => 'test'
+ Catalog::write('runtime', 'message.pluralRule', 'root', $data, array(
+ 'scope' => 'test'
));
$result = Message::translate('catalog', array('locale' => 'de'));
@@ -178,7 +177,7 @@ class MessageTest extends \lithium\test\Unit {
'The fish is {:color}.' => 'Der Fisch ist {:color}.',
'{:count} bike' => array('{:count} Fahrrad', '{:count} Fahrräder'),
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$expected = 'Der Fisch ist grün.';
$result = Message::translate('The fish is {:color}.', array(
@@ -200,11 +199,11 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'catalog' => 'Katalog',
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$data = array(
'catalog' => 'catalogue',
);
- Catalog::write('message', 'fr', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'fr', $data);
$expected = 'Katalog';
$result = Message::translate('catalog', array('locale' => 'de'));
@@ -219,7 +218,7 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'catalog' => 'Katalog',
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$result = Message::translate('catalog', array('locale' => 'de', 'noop' => true));
$this->assertNull($result);
@@ -229,7 +228,7 @@ class MessageTest extends \lithium\test\Unit {
$data = array(
'house' => array('Haus', 'Häuser')
);
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'message', 'de', $data);
$filters = Message::aliases();
$t = $filters['t'];
@@ -249,10 +248,8 @@ class MessageTest extends \lithium\test\Unit {
}
public function testAliasesSymmetry() {
- $data = array(
- 'house' => array('Haus', 'Häuser')
- );
- Catalog::write('message', 'de', $data, array('name' => 'runtime'));
+ $data = array('house' => array('Haus', 'Häuser'));
+ Catalog::write('runtime', 'message', 'de', $data);
$filters = Message::aliases();
$t = $filters['t'];
@@ -284,6 +281,33 @@ class MessageTest extends \lithium\test\Unit {
$result = $tn('house', 'houses', array('locale' => 'de'));
$this->assertNotEqual($expected, $result);
}
+
+ public function testCaching() {
+ $data = array('catalog' => 'Katalog');
+ Catalog::write('runtime', 'message', 'de', $data, array('scope' => 'foo'));
+
+ $this->assertFalse(Message::cache());
+
+ $result = Message::translate('catalog', array('locale' => 'de', 'scope' => 'foo'));
+ $this->assertEqual('Katalog', $result);
+
+ $cache = Message::cache();
+ $this->assertEqual('Katalog', $cache['foo']['de']['catalog']);
+
+ Message::cache(false);
+ $this->assertFalse(Message::cache());
+
+ Message::cache(array('foo' => array('de' => array('catalog' => '<Katalog>'))));
+ $result = Message::translate('catalog', array('locale' => 'de', 'scope' => 'foo'));
+ $this->assertEqual('<Katalog>', $result);
+
+ $options = array('locale' => 'de', 'scope' => 'foo', 'count' => 2);
+ $this->assertEqual('<Katalog>', Message::translate('catalog', $options));
+
+ Message::cache(false);
+ Message::cache(array('foo' => array('de' => array('catalog' => array('<Katalog>')))));
+ $this->assertNull(Message::translate('catalog', $options));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/catalog/AdapterTest.php b/libraries/lithium/tests/cases/g11n/catalog/AdapterTest.php
index c70b521..8b8a7bd 100644
--- a/libraries/lithium/tests/cases/g11n/catalog/AdapterTest.php
+++ b/libraries/lithium/tests/cases/g11n/catalog/AdapterTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\g11n\catalog;
-use \Exception;
-use \lithium\tests\mocks\g11n\catalog\MockAdapter;
+use Exception;
+use lithium\tests\mocks\g11n\catalog\MockAdapter;
class AdapterTest extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/g11n/catalog/adapter/CldrTest.php b/libraries/lithium/tests/cases/g11n/catalog/adapter/CldrTest.php
deleted file mode 100644
index 853c6d7..0000000
--- a/libraries/lithium/tests/cases/g11n/catalog/adapter/CldrTest.php
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\g11n\catalog\adapter;
-
-use \Exception;
-use \lithium\g11n\catalog\adapter\Cldr;
-
-class CldrTest extends \lithium\test\Unit {
-
- public $adapter;
-
- protected $_path;
-
- /**
- * Skip the test if data needed by the adapter cannot be found.
- *
- * @return void
- */
- public function skip() {
- $available = is_dir(LITHIUM_APP_PATH . '/resources/g11n/cldr');
- $this->skipIf(!$available, 'Data needed by the cldr adapter is not available.');
- }
-
- public function setUp() {
- $this->_path = $path = LITHIUM_APP_PATH . '/resources/g11n/cldr';
- $this->adapter = new Cldr(compact('path'));
- }
-
- public function testPathMustExist() {
- try {
- new Cldr(array('path' => $this->_path));
- $result = true;
- } catch (Exception $e) {
- $result = false;
- }
- $this->assert($result);
-
- try {
- new Cldr(array('path' => "{$this->_path}/i_do_not_exist"));
- $result = false;
- } catch (Exception $e) {
- $result = true;
- }
- $this->assert($result);
- }
-
- public function testReadLanguage() {
- $result = $this->adapter->read('language', 'de', null);
-
- $this->assertEqual($result['be']['translated'], 'Weißrussisch');
- $this->assertEqual($result['en']['translated'], 'Englisch');
- $this->assertEqual($result['fr']['translated'], 'Französisch');
-
- $result = $this->adapter->read('language', 'de_CH', null);
- $this->assertEqual($result['be']['translated'], 'Weissrussisch');
- }
-
- public function testReadScript() {
- $result = $this->adapter->read('script', 'de', null);
- $this->assertEqual($result['Cher']['translated'], 'Cherokee');
- $this->assertEqual($result['Hans']['translated'], 'Vereinfachte Chinesische Schrift');
- }
-
- public function testReadTerritory() {
- $result = $this->adapter->read('territory', 'de', null);
- $this->assertEqual($result['US']['translated'], 'Vereinigte Staaten');
- $this->assertEqual($result['FR']['translated'], 'Frankreich');
- }
-
- public function testReadCurrency() {
- $result = $this->adapter->read('currency', 'de', null);
- $this->assertEqual($result['DKK']['translated'], 'Dänische Krone');
- $this->assertEqual($result['USD']['translated'], 'US-Dollar');
- $this->assertEqual($result['EUR']['translated'], 'Euro');
- }
-
- public function testReadValidation() {
- $result = $this->adapter->read('validation', 'en_CA', null);
- $expected = '/^[ABCEGHJKLMNPRSTVXY]\d[A-Z][ ]?\d[A-Z]\d$/';
- $this->assertEqual($result['postalCode']['translated'], $expected);
-
- $result = $this->adapter->read('validation', 'en', null);
- $this->assertNull($result);
- }
-
- public function testReadWithScope() {
- $this->adapter = new Cldr(array('path' => $this->_path, 'scope' => 'li3_docs'));
-
- $result = $this->adapter->read('script', 'de', null);
- $this->assertFalse($result);
-
- $result = $this->adapter->read('script', 'de', 'li3_docs');
- $this->assertEqual($result['Cher']['translated'], 'Cherokee');
- $this->assertEqual($result['Hans']['translated'], 'Vereinfachte Chinesische Schrift');
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/catalog/adapter/CodeTest.php b/libraries/lithium/tests/cases/g11n/catalog/adapter/CodeTest.php
index d65df7e..85b719c 100644
--- a/libraries/lithium/tests/cases/g11n/catalog/adapter/CodeTest.php
+++ b/libraries/lithium/tests/cases/g11n/catalog/adapter/CodeTest.php
@@ -2,55 +2,70 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\g11n\catalog\adapter;
-use \Exception;
-use \lithium\g11n\catalog\adapter\Code;
+use Exception;
+use lithium\core\Libraries;
+use lithium\g11n\catalog\adapter\Code;
-if (false) {
- $t('simple 1');
+class CodeTest extends \lithium\test\Unit {
- $t('options 1', null, array('locale' => 'en'));
+ public $adapter;
- $t('replace 1 {:a}', array('a' => 'b'));
+ protected $_path;
- $t($test['invalid']);
- $t(32203);
- $t('invalid 1', $test['invalid']);
- $t('invalid 2', 32203);
- $t('invalid 3', 'invalid 3b');
+ public function setUp() {
+ $this->_path = $path = Libraries::get(true, 'resources') . '/tmp/tests';
+ $this->skipIf(!is_writable($this->_path), "{$this->_path} is not writable.");
- $t('escaping\n1');
- $t("escaping\n2");
- $t("escaping\r\n3");
- $t('escaping
- 4');
+ $this->adapter = new Code(compact('path'));
- $tn('singular simple 1', 'plural simple 1', 3);
- $tn('singular simple 2', 'plural simple 2');
+ $file = "{$this->_path}/a.php";
+ $data = <<<'EOD'
+<?php
+$t('simple 1');
- $t('mixed 1');
- $tn('mixed 1', 'plural mixed 1', 3);
+$t('options 1', null, array('locale' => 'en'));
- $t('mixed 2');
- $tn('mixed 2', 'plural mixed 2', 3);
- $t('mixed 2');
- $t('plural mixed 2');
-}
+$t('replace 1 {:a}', array('a' => 'b'));
-class CodeTest extends \lithium\test\Unit {
+$t($test['invalid']);
+$t(32203);
+$t('invalid 1', $test['invalid']);
+$t('invalid 2', 32203);
+$t('invalid 3', 'invalid 3b');
- public $adapter;
+$t('escaping\n1');
+$t("escaping\n2");
+$t("escaping\r\n3");
+$t('escaping
+ 4');
- protected $_path;
+$tn('singular simple 1', 'plural simple 1', 3);
+$tn('singular simple 2', 'plural simple 2');
- public function setUp() {
- $this->_path = $path = __DIR__;
- $this->adapter = new Code(compact('path'));
+$t('mixed 1');
+$tn('mixed 1', 'plural mixed 1', 3);
+
+$t('mixed 2');
+$tn('mixed 2', 'plural mixed 2', 3);
+$t('mixed 2');
+$t('plural mixed 2');
+?>
+EOD;
+ file_put_contents($file, $data);
+
+ $file = "{$this->_path}/a.html.php";
+ $data = <<<'EOD'
+<?=$t('simple 1 short'); ?>
+
+<?=$tn('singular simple 1 short', 'plural simple 1 short', 3); ?>
+EOD;
+ file_put_contents($file, $data);
}
public function tearDown() {
@@ -58,7 +73,7 @@ class CodeTest extends \lithium\test\Unit {
}
public function testPathMustExist() {
- $path = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $path = Libraries::get(true, 'resources') . '/tmp/tests';
try {
new Code(array('path' => $this->_path));
@@ -85,6 +100,14 @@ class CodeTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testReadMessageTemplateTSimpleShort() {
+ $results = $this->adapter->read('messageTemplate', 'root', null);
+
+ $expected = array('singular' => 'simple 1 short');
+ $result = $results['simple 1 short']['ids'];
+ $this->assertEqual($expected, $result);
+ }
+
public function testReadMessageTemplateTOptions() {
$results = $this->adapter->read('messageTemplate', 'root', null);
@@ -110,7 +133,6 @@ class CodeTest extends \lithium\test\Unit {
$result = isset($results[32203]);
$this->assertFalse($result);
-
$expected = array('singular' => 'invalid 1');
$result = $results['invalid 1']['ids'];
$this->assertEqual($expected, $result);
@@ -164,6 +186,18 @@ class CodeTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testReadMessageTemplateTnSimpleShort() {
+ $results = $this->adapter->read('messageTemplate', 'root', null);
+
+ $expected = 'singular simple 1 short';
+ $result = $results['singular simple 1 short']['ids']['singular'];
+ $this->assertEqual($expected, $result);
+
+ $expected = 'plural simple 1 short';
+ $result = $results['singular simple 1 short']['ids']['plural'];
+ $this->assertEqual($expected, $result);
+ }
+
public function testReadMessageTemplateTnT() {
$results = $this->adapter->read('messageTemplate', 'root', null);
diff --git a/libraries/lithium/tests/cases/g11n/catalog/adapter/GettextTest.php b/libraries/lithium/tests/cases/g11n/catalog/adapter/GettextTest.php
index a889858..ca55470 100644
--- a/libraries/lithium/tests/cases/g11n/catalog/adapter/GettextTest.php
+++ b/libraries/lithium/tests/cases/g11n/catalog/adapter/GettextTest.php
@@ -2,14 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\g11n\catalog\adapter;
-use \Exception;
-use \lithium\tests\mocks\g11n\catalog\adapter\MockGettext;
+use Exception;
+use lithium\core\Libraries;
+use lithium\tests\mocks\g11n\catalog\adapter\MockGettext;
class GettextTest extends \lithium\test\Unit {
@@ -18,13 +19,13 @@ class GettextTest extends \lithium\test\Unit {
protected $_path;
public function skip() {
- $path = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $path = Libraries::get(true, 'resources') . '/tmp/tests';
$message = "Path {$path} is not writable.";
$this->skipIf(!is_writable($path), $message);
}
public function setUp() {
- $this->_path = $path = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_path = $path = Libraries::get(true, 'resources') . '/tmp/tests';
mkdir("{$this->_path}/en/LC_MESSAGES", 0755, true);
mkdir("{$this->_path}/de/LC_MESSAGES", 0755, true);
$this->adapter = new MockGettext(compact('path'));
@@ -535,7 +536,7 @@ EOD;
$this->adapter->write('message', 'de', null, $data);
$result = file_get_contents("{$this->_path}/de/LC_MESSAGES/default.po");
- $expected = '#, fuzzy\nmsgid ""\nmsgstr ""\n"Project-Id';
+ $expected = 'msgstr ""\n"Project-Id';
$this->assertPattern("%{$expected}%", $result);
$expected = '"Project-Id-Version: PACKAGE VERSION\\\n"\n';
@@ -556,7 +557,7 @@ EOD;
$expected = '"MIME-Version: 1.0\\\n"\n';
$this->assertPattern("%{$expected}%", $result);
- $expected = '"Content-Type: text/plain; charset=CHARSET\\\n"\n';
+ $expected = '"Content-Type: text/plain; charset=UTF-8\\\n"\n';
$this->assertPattern("%{$expected}%", $result);
$expected = '"Content-Transfer-Encoding: 8bit\\\n"\n';
@@ -597,6 +598,32 @@ EOD;
$this->assertPattern('/' . preg_quote($po, '/') . '/', $result);
}
+ public function testWrittenPoHasShortFilePaths() {
+ $this->adapter->mo = false;
+
+ $data = array(
+ 'singular 1' => array(
+ 'id' => 'singular 1',
+ 'ids' => array('singular' => 'singular 1', 'plural' => 'plural 1'),
+ 'flags' => array(),
+ 'translated' => array('translated 1-0', 'translated 1-1'),
+ 'occurrences' => array(
+ array('file' => LITHIUM_APP_PATH . '/testa.php', 'line' => 22),
+ array('file' => '/testb.php', 'line' => 23)
+ ),
+ 'comments' => array()
+ )
+ );
+ $this->adapter->write('messageTemplate', 'root', null, $data);
+ $result = file_get_contents("{$this->_path}/message_default.pot");
+
+ $expected = '\#: /testa\.php:22';
+ $this->assertPattern("={$expected}=", $result);
+
+ $expected = '\#: /testb\.php:23';
+ $this->assertPattern("={$expected}=", $result);
+ }
+
public function testEscapeUnescape() {
$this->adapter->mo = false;
$file = "{$this->_path}/de/LC_MESSAGES/default.po";
@@ -638,7 +665,7 @@ EOD;
);
foreach ($chars as $unescaped => $escaped) {
- $ord = ord($unescaped);
+ $ord = decoct(ord($unescaped));
$catalog = array(
"this is the{$unescaped}message" => array(
@@ -656,13 +683,17 @@ msgstr "this is the{$escaped}translation"
EOD;
file_put_contents($file, $po);
$result = $this->adapter->read('message', 'de', null);
- $this->assertEqual($catalog, $result);
+ $message = "`{$unescaped}` (ASCII octal {$ord}) was not escaped to `{$escaped}`";
+ $message .= "\n{:message}";
+ $this->assertEqual($catalog, $result, $message);
unlink($file);
$this->adapter->write('message', 'de', null, $catalog);
$result = file_get_contents($file);
- $this->assertPattern('/' . preg_quote($po, '/') . '/', $result);
+ $message = "`{$escaped}` was not unescaped to `{$unescaped}` (ASCII octal {$ord})";
+ $message .= "\n{:message}";
+ $this->assertPattern('/' . preg_quote($po, '/') . '/', $result, $message);
unlink($file);
}
diff --git a/libraries/lithium/tests/cases/g11n/catalog/adapter/PhpTest.php b/libraries/lithium/tests/cases/g11n/catalog/adapter/PhpTest.php
index d95496e..e58abc8 100644
--- a/libraries/lithium/tests/cases/g11n/catalog/adapter/PhpTest.php
+++ b/libraries/lithium/tests/cases/g11n/catalog/adapter/PhpTest.php
@@ -2,14 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\g11n\catalog\adapter;
-use \Exception;
-use \lithium\g11n\catalog\adapter\Php;
+use Exception;
+use lithium\core\Libraries;
+use lithium\g11n\catalog\adapter\Php;
class PhpTest extends \lithium\test\Unit {
@@ -18,7 +19,7 @@ class PhpTest extends \lithium\test\Unit {
protected $_path;
public function skip() {
- $this->_path = $path = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $this->_path = $path = Libraries::get(true, 'resources') . '/tmp/tests';
$message = "{$path} is not writable.";
$this->skipIf(!is_writable($path), $message);
}
diff --git a/libraries/lithium/tests/cases/net/MessageTest.php b/libraries/lithium/tests/cases/net/MessageTest.php
new file mode 100644
index 0000000..c1d703a
--- /dev/null
+++ b/libraries/lithium/tests/cases/net/MessageTest.php
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\net;
+
+use lithium\net\Message;
+
+class MessageTest extends \lithium\test\Unit {
+
+ public $request = null;
+
+ public function setUp() {
+ $this->message = new Message();
+ }
+
+ public function testBody() {
+ $expected = "Part 1";
+ $result = $this->message->body('Part 1');
+ $this->assertEqual($expected, $result);
+
+ $expected = "Part 1\r\nPart 2";
+ $result = $this->message->body('Part 2');
+ $this->assertEqual($expected, $result);
+
+ $expected = "Part 1\r\nPart 2\r\nPart 3\r\nPart 4";
+ $result = $this->message->body(array('Part 3', 'Part 4'));
+ $this->assertEqual($expected, $result);
+
+ $expected = array('Part 1', 'Part 2', 'Part 3', 'Part 4');
+ $result = $this->message->body;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testBodyBuffer() {
+ $expected = array('P', 'a', 'r', 't', ' ', '1');
+ $result = $this->message->body('Part 1', array('buffer' => 1));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToArray() {
+ $expected = array(
+ 'scheme' => 'tcp',
+ 'host' => 'localhost',
+ 'port' => null,
+ 'path' => null,
+ 'username' => null,
+ 'password' => null,
+ 'body' => array(),
+ );
+ $result = $this->message->to('array');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToUrl() {
+ $expected = "tcp://localhost";
+ $result = $this->message->to('url');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToContext() {
+ $expected = array('tcp' => array('content' => null, 'ignore_errors' => true));
+ $result = $this->message->to('context');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToString() {
+ $expected = "woohoo";
+ $this->message->body($expected);
+ $result = (string) $this->message;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->message->to('string');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testConstruct() {
+ $expected = array(
+ 'scheme' => 'http',
+ 'host' => 'localhost',
+ 'port' => '80',
+ 'path' => null,
+ 'username' => null,
+ 'password' => null,
+ 'body' => array(),
+ );
+ $message = new Message($expected);
+ $result = $message->to('array');
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/BaseTest.php b/libraries/lithium/tests/cases/net/http/BaseTest.php
deleted file mode 100644
index 28b5e84..0000000
--- a/libraries/lithium/tests/cases/net/http/BaseTest.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\net\http;
-
-use \lithium\net\http\Base;
-
-class BaseTest extends \lithium\test\Unit {
-
- public $request = null;
-
- public function setUp() {
- $this->base = new Base();
- }
-
- public function testHeaderKey() {
- $expected = array(
- 'Host: localhost:80',
- );
- $result = $this->base->headers('Host: localhost:80');
- $this->assertEqual($expected, $result);
-
- $expected = 'localhost:80';
- $result = $this->base->headers('Host');
- $this->assertEqual($expected, $result);
-
- $expected = null;
- $result = $this->base->headers('Host', false);
- $this->assertEqual($expected, $result);
- }
-
- public function testHeaderKeyValue() {
- $expected = array(
- 'Connection: Close',
- );
- $result = $this->base->headers('Connection', 'Close');
- $this->assertEqual($expected, $result);
- }
-
- public function testHeaderArrayValue() {
- $expected = array(
- 'User-Agent: Mozilla/5.0 (Lithium)',
- );
- $result = $this->base->headers(array('User-Agent: Mozilla/5.0 (Lithium)'));
- $this->assertEqual($expected, $result);
- }
-
- public function testHeaderArrayKeyValue() {
- $expected = array(
- 'Cache-Control: no-cache'
- );
- $result = $this->base->headers(array('Cache-Control' => 'no-cache'));
- $this->assertEqual($expected, $result);
- }
-
- public function testBody() {
- $expected = "Part 1";
- $result = $this->base->body('Part 1');
- $this->assertEqual($expected, $result);
-
- $expected = "Part 1\r\nPart 2";
- $result = $this->base->body('Part 2');
- $this->assertEqual($expected, $result);
-
- $expected = "Part 1\r\nPart 2\r\nPart 3\r\nPart 4";
- $result = $this->base->body(array('Part 3', 'Part 4'));
- $this->assertEqual($expected, $result);
-
- $expected = array('Part 1', 'Part 2', 'Part 3', 'Part 4');
- $result = $this->base->body;
- $this->assertEqual($expected, $result);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/MediaTest.php b/libraries/lithium/tests/cases/net/http/MediaTest.php
index caded7f..27a1a71 100644
--- a/libraries/lithium/tests/cases/net/http/MediaTest.php
+++ b/libraries/lithium/tests/cases/net/http/MediaTest.php
@@ -2,43 +2,56 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\http;
-use \lithium\net\http\Media;
-use \lithium\action\Request;
-use \lithium\action\Response;
-use \lithium\core\Libraries;
+use lithium\net\http\Media;
+use lithium\action\Request;
+use lithium\action\Response;
+use lithium\core\Libraries;
+use lithium\data\entity\Record;
+use lithium\data\collection\RecordSet;
class MediaTest extends \lithium\test\Unit {
/**
+ * Reset the `Media` class to its default state.
+ *
+ * @return void
+ */
+ public function setUp() {
+ Media::reset();
+ }
+
+ /**
* Tests setting, getting and removing custom media types.
*
* @return void
*/
public function testMediaTypes() {
- $result = Media::types();
-
- $this->assertTrue(is_array($result));
- $this->assertTrue(in_array('json', $result));
- $this->assertFalse(in_array('my', $result));
+ // Get a list of all available media types:
+ $types = Media::types(); // returns array('html', 'json', 'rss', ...);
- $this->assertEqual($result, Media::formats());
+ $expected = array(
+ 'html', 'htm', 'form', 'json', 'rss', 'atom', 'css', 'js', 'text', 'txt', 'xml'
+ );
+ $this->assertEqual($expected, $types);
+ $this->assertEqual($expected, Media::formats());
$result = Media::type('json');
$expected = 'application/json';
$this->assertEqual($expected, $result['content']);
$expected = array(
- 'view' => false, 'layout' => false, 'encode' => 'json_encode', 'decode' => 'json_decode'
+ 'cast' => true, 'encode' => 'json_encode', 'decode' => $result['options']['decode']
);
$this->assertEqual($expected, $result['options']);
- Media::type('my', 'text/x-my', array('view' => '\my\custom\View', 'layout' => false));
+ // Add a custom media type with a custom view class:
+ Media::type('my', 'text/x-my', array('view' => 'my\custom\View', 'layout' => false));
$result = Media::types();
$this->assertTrue(in_array('my', $result));
@@ -48,14 +61,12 @@ class MediaTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result['content']);
$expected = array(
- 'view' => '\my\custom\View',
- 'template' => null,
- 'layout' => null,
- 'encode' => null,
- 'decode' => null
+ 'view' => 'my\custom\View', 'template' => null, 'layout' => null,
+ 'encode' => null, 'decode' => null, 'cast' => true, 'conditions' => array()
);
$this->assertEqual($expected, $result['options']);
+ // Remove a custom media type:
Media::type('my', false);
$result = Media::types();
$this->assertFalse(in_array('my', $result));
@@ -70,6 +81,14 @@ class MediaTest extends \lithium\test\Unit {
$this->assertNull(Media::type('application/foo'));
$this->assertEqual('js', Media::type('application/javascript'));
$this->assertEqual('html', Media::type('*/*'));
+ $this->assertEqual('json', Media::type('application/json'));
+ $this->assertEqual('json', Media::type('application/json; charset=UTF-8'));
+
+ $result = Media::type('json');
+ $expected = array('content' => 'application/json', 'options' => array(
+ 'cast' => true, 'encode' => 'json_encode', 'decode' => $result['options']['decode']
+ ));
+ $this->assertEqual($expected, $result);
}
public function testAssetTypeHandling() {
@@ -108,6 +127,8 @@ class MediaTest extends \lithium\test\Unit {
Media::assets('my', false);
$result = Media::assets('my');
$this->assertNull($result);
+
+ $this->assertEqual('/foo.exe', Media::asset('foo.exe', 'bar'));
}
public function testAssetPathGeneration() {
@@ -139,6 +160,9 @@ class MediaTest extends \lithium\test\Unit {
'check' => 'true', 'base' => 'foo', 'timestamp' => true
));
$this->assertPattern('%^foo/css/debug\.css\?type=test&\d+$%', $result);
+
+ $file = Media::path('css/debug.css', 'bar');
+ $this->assertTrue(file_exists($file));
}
public function testCustomAssetPathGeneration() {
@@ -214,13 +238,11 @@ class MediaTest extends \lithium\test\Unit {
$data = array('something');
Media::render($response, $data);
- $expected = array('Content-type: application/json');
$result = $response->headers();
- $this->assertEqual($expected, $result);
+ $this->assertEqual(array('Content-type: application/json; charset=UTF-8'), $result);
- $expected = json_encode($data);
$result = $response->body();
- $this->assertEqual($expected, $result);
+ $this->assertEqual(json_encode($data), $result);
}
/**
@@ -229,9 +251,9 @@ class MediaTest extends \lithium\test\Unit {
* @return void
*/
public function testDecode() {
- $data = (object) array('movies' => array(
- (object) array('name' => 'Shaun of the Dead', 'year' => 2004),
- (object) array('name' => 'V for Vendetta', 'year' => 2005)
+ $data = array('movies' => array(
+ array('name' => 'Shaun of the Dead', 'year' => 2004),
+ array('name' => 'V for Vendetta', 'year' => 2005)
));
$encoded = '{"movies":[{"name":"Shaun of the Dead","year":2004},';
$encoded .= '{"name":"V for Vendetta","year":2005}]}';
@@ -242,7 +264,7 @@ class MediaTest extends \lithium\test\Unit {
public function testCustomEncodeHandler() {
$response = new Response();
- $response->type = 'csv';
+ $response->type('csv');
Media::type('csv', 'application/csv', array('encode' => function($data) {
ob_start();
@@ -266,8 +288,7 @@ class MediaTest extends \lithium\test\Unit {
$this->assertEqual(array($expected), $result);
$result = $response->headers['Content-type'];
- $expected = 'application/csv';
- $this->assertEqual($expected, $result);
+ $this->assertEqual('application/csv; charset=UTF-8', $result);
}
/**
@@ -277,12 +298,11 @@ class MediaTest extends \lithium\test\Unit {
*/
public function testPlainTextOutput() {
$response = new Response();
- $response->type = 'text';
+ $response->type('text');
Media::render($response, "Hello, world!");
- $expected = array("Hello, world!");
$result = $response->body;
- $this->assertEqual($expected, $result);
+ $this->assertEqual(array("Hello, world!"), $result);
}
/**
@@ -293,9 +313,9 @@ class MediaTest extends \lithium\test\Unit {
*/
public function testUndhandledContent() {
$response = new Response();
- $response->type = 'bad';
+ $response->type('bad');
- $this->expectException("Unhandled media type 'bad'");
+ $this->expectException("Unhandled media type `bad`.");
Media::render($response, array('foo' => 'bar'));
$result = $response->body;
@@ -310,9 +330,9 @@ class MediaTest extends \lithium\test\Unit {
*/
public function testUnregisteredContentHandler() {
$response = new Response();
- $response->type = 'xml';
+ $response->type('xml');
- $this->expectException("Unhandled media type 'xml'");
+ $this->expectException("Unhandled media type `xml`.");
Media::render($response, array('foo' => 'bar'));
$result = $response->body;
@@ -355,6 +375,7 @@ class MediaTest extends \lithium\test\Unit {
* @return void
*/
public function testRequestOptionMerging() {
+ Media::type('custom', 'text/x-custom');
$request = new Request();
$request->params['foo'] = 'bar';
@@ -364,7 +385,7 @@ class MediaTest extends \lithium\test\Unit {
Media::render($response, null, compact('request') + array(
'layout' => false,
'template' => false,
- 'encode' => function($data, $handler, $options) { return $options['foo']; }
+ 'encode' => function($data, $handler) { return $handler['request']->foo; }
));
$this->assertEqual(array('bar'), $response->body);
}
@@ -376,13 +397,14 @@ class MediaTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$this->assertEqual($result, Media::to('json', $data));
+ $this->assertNull(Media::encode('badness', $data));
- $result = Media::encode('badness', $data);
- $this->assertNull($result);
+ $result = Media::decode('json', $expected);
+ $this->assertEqual($data, $result);
}
public function testRenderWithOptionsMerging() {
- $base = LITHIUM_APP_PATH . '/resources/tmp';
+ $base = Libraries::get(true, 'resources') . '/tmp';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
$request = new Request();
@@ -391,13 +413,154 @@ class MediaTest extends \lithium\test\Unit {
$response = new Response();
$response->type = 'html';
- Media::render($response, null, compact('request') + array(
- 'layout' => false,
- 'template' => 'home',
- ));
- $this->assertPattern('/home/', $response->body());
+ $this->expectException('/Template not found/');
+ Media::render($response, null, compact('request'));
$this->_cleanUp();
}
+
+ public function testCustomWebroot() {
+ Libraries::add('defaultStyleApp', array('path' => LITHIUM_APP_PATH, 'bootstrap' => false));
+ $this->assertEqual(LITHIUM_APP_PATH . '/webroot', Media::webroot('defaultStyleApp'));
+
+ Libraries::add('customWebRootApp', array(
+ 'path' => LITHIUM_APP_PATH,
+ 'webroot' => LITHIUM_APP_PATH,
+ 'bootstrap' => false
+ ));
+ $this->assertEqual(LITHIUM_APP_PATH, Media::webroot('customWebRootApp'));
+
+ Libraries::remove('defaultStyleApp');
+ Libraries::remove('customWebRootApp');
+ $this->assertNull(Media::webroot('defaultStyleApp'));
+ }
+
+ /**
+ * Tests that the `Media` class' configuration can be reset to its default state.
+ *
+ * @return void
+ */
+ public function testStateReset() {
+ $this->assertFalse(in_array('foo', Media::types()));
+
+ Media::type('foo', 'text/x-foo');
+ $this->assertTrue(in_array('foo', Media::types()));
+
+ Media::reset();
+ $this->assertFalse(in_array('foo', Media::types()));
+ }
+
+ public function testEncodeRecordSet() {
+ $data = new RecordSet(array('data' => array(
+ 1 => new Record(array('data' => array('id' => 1, 'foo' => 'bar'))),
+ 2 => new Record(array('data' => array('id' => 2, 'foo' => 'baz'))),
+ 3 => new Record(array('data' => array('id' => 3, 'baz' => 'dib')))
+ )));
+ $json = '{"1":{"id":1,"foo":"bar"},"2":{"id":2,"foo":"baz"},"3":{"id":3,"baz":"dib"}}';
+ $this->assertEqual($json, Media::encode(array('encode' => 'json_encode'), $data));
+ }
+
+ /**
+ * Tests that calling `Media::type()` to retrieve the details of a type that is aliased to
+ * another type, automatically resolves to the settings of the type being pointed at.
+ *
+ * @return void
+ */
+ public function testTypeAliasResolution() {
+ $resolved = Media::type('text');
+ $this->assertEqual('text/plain', $resolved['content']);
+ unset($resolved['options']['encode']);
+
+ $result = Media::type('txt');
+ unset($result['options']['encode']);
+ $this->assertEqual($resolved, $result);
+ }
+
+ public function testQueryUndefinedAssetTypes() {
+ $base = Media::path('index.php', 'generic');
+ $result = Media::path('index.php', 'foo');
+ $this->assertEqual($result, $base);
+
+ $base = Media::asset('/bar', 'generic');
+ $result = Media::asset('/bar', 'foo');
+ $this->assertEqual($result, $base);
+ }
+
+ public function testGetLibraryWebroot() {
+ $this->assertTrue(is_dir(Media::webroot(true)));
+ $this->assertNull(Media::webroot('foobar'));
+
+ Libraries::add('foobar', array('path' => __DIR__, 'webroot' => __DIR__));
+ $this->assertEqual(__DIR__, Media::webroot('foobar'));
+ Libraries::remove('foobar');
+ }
+
+ /**
+ * Tests that the `Response` object can be directly modified from a templating class or encode
+ * function.
+ *
+ * @return void
+ */
+ public function testResponseModification() {
+ Media::type('my', 'text/x-my', array('view' => 'lithium\tests\mocks\net\http\Template'));
+ $response = new Response();
+
+ Media::render($response, null, array('type' => 'my'));
+ $this->assertEqual('Value', $response->headers('Custom'));
+ }
+
+ /**
+ * Tests that `Media::asset()` will not prepend path strings with the base application path if
+ * it has already been prepended.
+ *
+ * @return void
+ */
+ public function testDuplicateBasePathCheck() {
+ $result = Media::asset('/foo/bar/image.jpg', 'image', array('base' => '/bar'));
+ $this->assertEqual('/bar/foo/bar/image.jpg', $result);
+
+ $result = Media::asset('/foo/bar/image.jpg', 'image', array('base' => '/foo/bar'));
+ $this->assertEqual('/foo/bar/image.jpg', $result);
+
+ $result = Media::asset('foo/bar/image.jpg', 'image', array('base' => 'foo'));
+ $this->assertEqual('foo/img/foo/bar/image.jpg', $result);
+
+ $result = Media::asset('/foo/bar/image.jpg', 'image', array('base' => ''));
+ $this->assertEqual('/foo/bar/image.jpg', $result);
+ }
+
+ public function testContentNegotiationByType() {
+ $this->assertEqual('html', Media::type('text/html'));
+
+ Media::type('jsonp', 'text/html', array(
+ 'conditions' => array('type' => true)
+ ));
+ $this->assertEqual(array('jsonp', 'html'), Media::type('text/html'));
+
+ $config = array('env' => array('HTTP_ACCEPT' => 'text/html,text/plain;q=0.5'));
+ $request = new Request($config);
+ $request->params = array('type' => 'jsonp');
+ $this->assertEqual('jsonp', Media::negotiate($request));
+
+ $request = new Request($config);
+ $this->assertEqual('html', Media::negotiate($request));
+ }
+
+ public function testContentNegotiationByUserAgent() {
+ Media::type('iphone', 'application/xhtml+xml', array(
+ 'conditions' => array('mobile' => true)
+ ));
+ $request = new Request(array('env' => array(
+ 'HTTP_USER_AGENT' => 'Safari',
+ 'HTTP_ACCEPT' => 'application/xhtml+xml,text/html'
+ )));
+ $this->assertEqual('html', Media::negotiate($request));
+
+ $request = new Request(array('env' => array(
+ 'HTTP_USER_AGENT' => 'iPhone',
+ 'HTTP_ACCEPT' => 'application/xhtml+xml,text/html'
+ )));
+ $this->assertEqual('iphone', Media::negotiate($request));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/MessageTest.php b/libraries/lithium/tests/cases/net/http/MessageTest.php
new file mode 100644
index 0000000..f86969e
--- /dev/null
+++ b/libraries/lithium/tests/cases/net/http/MessageTest.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\net\http;
+
+use lithium\net\http\Message;
+
+class MessageTest extends \lithium\test\Unit {
+
+ public $request = null;
+
+ public function setUp() {
+ $this->message = new Message();
+ }
+
+ public function testHeaderKey() {
+ $expected = array(
+ 'Host: localhost:80',
+ );
+ $result = $this->message->headers('Host: localhost:80');
+ $this->assertEqual($expected, $result);
+
+ $expected = 'localhost:80';
+ $result = $this->message->headers('Host');
+ $this->assertEqual($expected, $result);
+
+ $result = $this->message->headers('Host', false);
+ $this->assertFalse($result);
+ }
+
+ public function testHeaderKeyValue() {
+ $expected = array(
+ 'Connection: Close',
+ );
+ $result = $this->message->headers('Connection', 'Close');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHeaderArrayValue() {
+ $expected = array('User-Agent: Mozilla/5.0');
+ $result = $this->message->headers(array('User-Agent: Mozilla/5.0'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHeaderArrayKeyValue() {
+ $expected = array(
+ 'Cache-Control: no-cache'
+ );
+ $result = $this->message->headers(array('Cache-Control' => 'no-cache'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testType() {
+ $this->assertEqual('json', $this->message->type("json"));
+ $this->assertEqual('json', $this->message->type());
+
+ $expected = 'json';
+ $result = $this->message->type("application/json; charset=UTF-8");
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/RequestTest.php b/libraries/lithium/tests/cases/net/http/RequestTest.php
index 71c4d8e..d01846f 100644
--- a/libraries/lithium/tests/cases/net/http/RequestTest.php
+++ b/libraries/lithium/tests/cases/net/http/RequestTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\http;
-use \lithium\net\http\Request;
+use lithium\net\http\Request;
class RequestTest extends \lithium\test\Unit {
@@ -58,7 +58,7 @@ class RequestTest extends \lithium\test\Unit {
$expected = array(
'Host: localhost:443',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'Header: Value'
);
$result = $request->headers();
@@ -117,25 +117,29 @@ class RequestTest extends \lithium\test\Unit {
public function testToString() {
$expected = join("\r\n", array(
'GET / HTTP/1.1',
- 'Host: localhost:80',
+ 'Host: localhost',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'', ''
));
$result = (string) $this->request;
$this->assertEqual($expected, $result);
+
+ $result = $this->request->to('string');
+ $this->assertEqual($expected, $result);
}
public function testToStringWithAuth() {
- $request = new Request(array('auth' => array(
- 'method' => 'Basic',
- 'username' => 'root', 'password' => 'something'
- )));
+ $request = new Request(array(
+ 'auth' => 'Basic',
+ 'username' => 'root',
+ 'password' => 'something'
+ ));
$expected = join("\r\n", array(
'GET / HTTP/1.1',
- 'Host: localhost:80',
+ 'Host: localhost',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'Authorization: Basic ' . base64_encode('root:something'),
'', ''
));
@@ -143,12 +147,33 @@ class RequestTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testToContextWithAuth() {
+ $request = new Request(array(
+ 'auth' => 'Basic',
+ 'username' => 'Aladdin',
+ 'password' => 'open sesame'
+ ));
+ $expected = array('http' => array(
+ 'method' => 'GET',
+ 'header' => array(
+ 'Host: localhost',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='
+ ),
+ 'content' => '',
+ 'protocol_version' => '1.1',
+ 'ignore_errors' => true
+ ));
+ $this->assertEqual($expected, $request->to('context'));
+ }
+
public function testToStringWithBody() {
$expected = join("\r\n", array(
'GET / HTTP/1.1',
- 'Host: localhost:80',
+ 'Host: localhost',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'Content-Length: 11',
'', 'status=cool'
));
@@ -156,6 +181,57 @@ class RequestTest extends \lithium\test\Unit {
$result = (string) $this->request;
$this->assertEqual($expected, $result);
}
+
+ public function testToArray() {
+ $expected = array(
+ 'method' => 'GET',
+ 'params' => array(),
+ 'headers' => array(
+ 'Host' => 'localhost',
+ 'Connection' => 'Close',
+ 'User-Agent' => 'Mozilla/5.0'
+ ),
+ 'cookies' => array(),
+ 'protocol' => 'HTTP/1.1',
+ 'version' => '1.1',
+ 'body' => array(),
+ 'scheme' => 'http',
+ 'host' => 'localhost',
+ 'port' => NULL,
+ 'path' => '/',
+ 'username' => NULL,
+ 'password' => NULL,
+ );
+ $result = $this->request->to('array');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToUrl() {
+ $expected = 'http://localhost/';
+ $result = $this->request->to('url');
+ $this->assertEqual($expected, $result);
+
+ $this->request = new Request(array('scheme' => 'https', 'port' => 443));
+ $expected = 'https://localhost:443/';
+ $result = $this->request->to('url');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToContext() {
+ $expected = array('http' => array(
+ 'method' => 'GET',
+ 'content' => '',
+ 'header' => array(
+ 'Host: localhost',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0'
+ ),
+ 'protocol_version' => '1.1',
+ 'ignore_errors' => true
+ ));
+ $result = $this->request->to('context');
+ $this->assertEqual($expected, $result);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/ResponseTest.php b/libraries/lithium/tests/cases/net/http/ResponseTest.php
index 0815352..a88bd96 100644
--- a/libraries/lithium/tests/cases/net/http/ResponseTest.php
+++ b/libraries/lithium/tests/cases/net/http/ResponseTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\http;
-use \lithium\net\http\Response;
+use lithium\net\http\Response;
class ResponseTest extends \lithium\test\Unit {
@@ -43,9 +43,30 @@ class ResponseTest extends \lithium\test\Unit {
$result = $response->status('See Other');
$this->assertEqual($expected, $result);
- $expected = false;
$result = $response->status('foobar');
- $this->assertEqual($expected, $result);
+ $this->assertFalse($result);
+ }
+
+ public function testParsingContentTypeWithEncoding() {
+ $response = new Response(array('headers' => array(
+ 'Content-Type' => 'text/xml;charset=UTF-8'
+ )));
+ $this->assertEqual('text/xml', $response->type);
+ $this->assertEqual('UTF-8', $response->encoding);
+
+ $response = new Response(array('headers' => array(
+ 'Content-Type' => 'text/xml;charset=UTF-8'
+ )));
+ $this->assertEqual('text/xml', $response->type);
+ $this->assertEqual('UTF-8', $response->encoding);
+ }
+
+ public function testConstructionWithBody() {
+ $response = new Response(array('message' => "Content-type: image/jpeg\r\n\r\nimage data"));
+ $this->assertEqual("image data", $response->body());
+
+ $response = new Response(array('body' => "image data"));
+ $this->assertEqual("image data", $response->body());
}
public function testParseMessage() {
@@ -53,21 +74,53 @@ class ResponseTest extends \lithium\test\Unit {
'HTTP/1.1 200 OK',
'Header: Value',
'Connection: close',
- 'Content-Type: text/html;charset=UTF-8',
+ 'Content-Type: text/html;charset=iso-8859-1',
'',
'Test!'
));
$response = new Response(compact('message'));
$this->assertEqual($message, (string) $response);
+ $this->assertEqual('ISO-8859-1', $response->encoding);
- $message = 'Invalid Message';
- $expected = join("\r\n", array(
+ $body = 'Not a Message';
+ $expected = join("\r\n", array('HTTP/1.1 200 OK', '', '', 'Not a Message'));
+ $response = new Response(compact('body'));
+ $this->assertEqual($expected, (string) $response);
+ }
+
+ public function testMessageContentTypeParsing() {
+ // Content type WITHOUT space between type and charset
+ $message = join("\r\n", array(
'HTTP/1.1 200 OK',
- '', '', ''
+ 'Content-Type: application/json;charset=iso-8859-1',
+ '',
+ 'Test!'
));
- $response = new Response(compact('message'));
- $this->assertEqual($expected, (string) $response);
+ $response = new Response(array('message' => $message));
+ $this->assertEqual('application/json', $response->type);
+ $this->assertEqual('ISO-8859-1', $response->encoding);
+
+ // Content type WITH ONE space between type and charset
+ $message = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Content-Type: application/json; charset=iso-8859-1',
+ '',
+ 'Test!'
+ ));
+ $response = new Response(array('message' => $message));
+ $this->assertEqual('application/json', $response->type);
+ $this->assertEqual('ISO-8859-1', $response->encoding);
+
+ $message = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Content-Type: application/json; charset=iso-8859-1',
+ '',
+ 'Test!'
+ ));
+ $response = new Response(array('message' => $message));
+ $this->assertEqual('application/json', $response->type);
+ $this->assertEqual('ISO-8859-1', $response->encoding);
}
public function testEmptyResponse() {
@@ -96,7 +149,7 @@ class ResponseTest extends \lithium\test\Unit {
'Content-Type' => 'text/html;charset=UTF-8'
),
'type' => 'text/html',
- 'charset' => 'UTF-8',
+ 'encoding' => 'UTF-8',
'body' => 'Test!'
);
$response = new Response($config);
@@ -121,7 +174,8 @@ class ResponseTest extends \lithium\test\Unit {
'b7',
'{"total_rows":1,"offset":0,"rows":[',
'{"id":"88989cafcd81b09f81078eb523832e8e","key":"gwoo","value":' .
- '{"author":"gwoo","language":"php","preview":"test","created":"2009-10-27 12:14:12"}}',
+ '{"author":"gwoo","language":"php","preview":"test",' .
+ '"created":"2009-10-27 12:14:12"}}',
'4',
'',
']}',
@@ -136,27 +190,23 @@ class ResponseTest extends \lithium\test\Unit {
$expected = join("\r\n", array(
'{"total_rows":1,"offset":0,"rows":[',
- '{"id":"88989cafcd81b09f81078eb523832e8e","key":"gwoo","value":'.
- '{"author":"gwoo","language":"php","preview":"test","created":"2009-10-27 12:14:12"}}',
+ '{"id":"88989cafcd81b09f81078eb523832e8e","key":"gwoo","value":' .
+ '{"author":"gwoo","language":"php","preview":"test",' .
+ '"created":"2009-10-27 12:14:12"}}',
']}',
));
- $result = $response->body();
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $response->body());
+
+ $message = $headers . join("\r\n", array('body'));
- $message = $headers . join("\r\n", array(
- 'body'
- ));
- $expected = 'body';
$response = new Response(compact('message'));
$result = $response->body();
- $this->assertEqual($expected, $result);
+ $this->assertEqual('body', $result);
- $message = $headers . join("\r\n", array(
- '[part one];',
- '[part two]'
- ));
+ $message = $headers . join("\r\n", array('[part one];', '[part two]'));
$expected = '[part two]';
$response = new Response(compact('message'));
+
$result = $response->body();
$this->assertEqual($expected, $result);
@@ -174,6 +224,13 @@ class ResponseTest extends \lithium\test\Unit {
$result = $response->body();
$this->assertEqual($expected, $result);
}
+
+ public function testTypeHeader() {
+ $response = new Response(array('type' => 'application/json'));
+ $result = (string) $response;
+ $this->assertPattern('/^HTTP\/1\.1 200 OK/', $result);
+ $this->assertPattern('/Content-Type: application\/json\s+$/ms', $result);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/RouteTest.php b/libraries/lithium/tests/cases/net/http/RouteTest.php
index 5ca6af3..a3a94da 100644
--- a/libraries/lithium/tests/cases/net/http/RouteTest.php
+++ b/libraries/lithium/tests/cases/net/http/RouteTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\http;
-use \lithium\net\http\Route;
-use \lithium\net\http\Request;
+use lithium\action\Request;
+use lithium\net\http\Route;
class RouteTest extends \lithium\test\Unit {
@@ -51,11 +51,13 @@ class RouteTest extends \lithium\test\Unit {
$request->url = '/';
$result = $route->parse($request);
- $this->assertEqual($params, $result);
+ $this->assertEqual($params, $result->params);
+ $this->assertEqual(array('controller'), $result->persist);
$request->url = '';
$result = $route->parse($request);
- $this->assertEqual($params, $result);
+ $this->assertEqual($params, $result->params);
+ $this->assertEqual(array('controller'), $result->persist);
$request->url = '/posts';
$this->assertFalse($route->parse($request));
@@ -90,14 +92,15 @@ class RouteTest extends \lithium\test\Unit {
public function testSimpleRouteParsing() {
$route = new Route(array('template' => '/{:controller}'));
$request = new Request();
+ $default = array('action' => 'index');
$request->url = '/posts';
$result = $route->parse($request);
- $this->assertEqual(array('controller' => 'posts', 'action' => 'index'), $result);
+ $this->assertEqual(array('controller' => 'posts') + $default, $result->params);
$request->url = '/users';
$result = $route->parse($request);
- $this->assertEqual(array('controller' => 'users', 'action' => 'index'), $result);
+ $this->assertEqual(array('controller' => 'users') + $default, $result->params);
$request->url = '/users/index';
$this->assertFalse($route->parse($request));
@@ -131,26 +134,28 @@ class RouteTest extends \lithium\test\Unit {
public function testRouteParsingWithOptionalParam() {
$route = new Route(array('template' => '/{:controller}/{:action}'));
$request = new Request();
+ $default = array('action' => 'index');
$request->url = '/posts';
$result = $route->parse($request);
- $this->assertEqual(array('controller' => 'posts', 'action' => 'index'), $result);
+ $this->assertEqual(array('controller' => 'posts') + $default, $result->params);
$request->url = '/users';
$result = $route->parse($request);
- $this->assertEqual(array('controller' => 'users', 'action' => 'index'), $result);
+ $this->assertEqual(array('controller' => 'users') + $default, $result->params);
$request->url = '/1';
$result = $route->parse($request);
- $this->assertEqual(array('controller' => '1', 'action' => 'index'), $result);
+ $this->assertEqual(array('controller' => '1') + $default, $result->params);
$request->url = '/users/index';
$result = $route->parse($request);
- $this->assertEqual(array('controller' => 'users', 'action' => 'index'), $result);
+ $this->assertEqual(array('controller' => 'users') + $default, $result->params);
$request->url = '/users/view';
$result = $route->parse($request);
- $this->assertEqual(array('controller' => 'users', 'action' => 'view'), $result);
+ $expected = array('controller' => 'users', 'action' => 'view');
+ $this->assertEqual($expected, $result->params);
$request->url = '/users/view/5';
$this->assertFalse($route->parse($request));
@@ -168,20 +173,20 @@ class RouteTest extends \lithium\test\Unit {
$request->url = '/posts';
$result = $route->parse($request);
$expected = array('controller' => 'posts', 'action' => 'index', 'id' => null);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$request->url = '/posts/index';
$result = $route->parse($request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$request->url = '/posts/index/';
$result = $route->parse($request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$request->url = '/posts/view/5';
$result = $route->parse($request);
$expected = array('controller' => 'posts', 'action' => 'view', 'id' => '5');
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$request->url = '/';
$this->assertFalse($route->parse($request));
@@ -196,27 +201,22 @@ class RouteTest extends \lithium\test\Unit {
'params' => array('id' => null)
));
$request = new Request();
+ $default = array('controller' => 'posts');
$request->url = '/posts/view/5.xml';
$result = $route->parse($request);
- $expected = array(
- 'controller' => 'posts', 'action' => 'view', 'id' => '5', 'type' => 'xml'
- );
- $this->assertEqual($expected, $result);
+ $expected = array('action' => 'view', 'id' => '5', 'type' => 'xml') + $default;
+ $this->assertEqual($expected, $result->params);
$request->url = '/posts/index.xml';
$result = $route->parse($request);
- $expected = array(
- 'controller' => 'posts', 'action' => 'index', 'id' => '', 'type' => 'xml'
- );
- $this->assertEqual($expected, $result);
+ $expected = array('action' => 'index', 'id' => '', 'type' => 'xml') + $default;
+ $this->assertEqual($expected, $result->params);
$request->url = '/posts.xml';
$result = $route->parse($request);
- $expected = array(
- 'controller' => 'posts', 'action' => 'index', 'id' => '', 'type' => 'xml'
- );
- $this->assertEqual($expected, $result);
+ $expected = array('action' => 'index', 'id' => '', 'type' => 'xml') + $default;
+ $this->assertEqual($expected, $result->params);
}
public function testRouteMatchingWithEmptyTrailingParams() {
@@ -238,7 +238,7 @@ class RouteTest extends \lithium\test\Unit {
$expected = array(
'controller' => 'posts', 'action' => 'index', 'args' => array('foo', 'bar')
);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
}
public function testStaticRouteMatching() {
@@ -256,11 +256,11 @@ class RouteTest extends \lithium\test\Unit {
$request->url = '/login';
$result = $route->parse($request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$request->url = 'login';
$result = $route->parse($request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
}
/**
@@ -282,7 +282,7 @@ class RouteTest extends \lithium\test\Unit {
$result = $route->parse($request);
$expected = array('controller' => 'posts', 'action' => 'index');
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$result = $route->match(array('controller' => 'posts', 'action' => 'index'));
$expected = '/posts';
@@ -307,8 +307,11 @@ class RouteTest extends \lithium\test\Unit {
'params' => array('action' => 'view'),
'defaults' => array('action' => 'view'),
'match' => array(),
+ 'meta' => array(),
'keys' => array('controller' => 'controller', 'action' => 'action'),
- 'subPatterns' => array()
+ 'subPatterns' => array(),
+ 'persist' => array('controller'),
+ 'handler' => null
);
$this->assertEqual($expected, $result);
}
@@ -327,8 +330,9 @@ class RouteTest extends \lithium\test\Unit {
'match' => array('controller' => 'users', 'action' => 'index'),
'defaults' => array('controller' => 'users'),
'keys' => array('user' => 'user'),
- 'options' => array('compile' => false, 'wrap' => false)
+ 'compile' => false
));
+
$result = $route->match(array('controller' => 'users', 'user' => 'alke'));
$expected = '/users/alke';
$this->assertEqual($expected, $result);
@@ -338,11 +342,11 @@ class RouteTest extends \lithium\test\Unit {
$expected = array('controller' => 'users', 'action' => 'index', 'user' => 'alke');
$result = $route->parse($request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$request->url = '/u/alke';
$result = $route->parse($request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
}
/**
@@ -358,7 +362,7 @@ class RouteTest extends \lithium\test\Unit {
$expected = array('controller' => 'users', 'action' => 'view', 'user' => '10');
$result = $route->parse($request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
$request->url = '/users/view/my_login';
$result = $route->parse($request);
@@ -406,6 +410,113 @@ class RouteTest extends \lithium\test\Unit {
$this->assertFalse($route->match(array('controller' => 'posts', 'id' => '009')));
}
+
+ /**
+ * Tests that route templates with elements containing repetition patterns are correctly parsed.
+ *
+ * @return void
+ */
+ public function testPatternsWithRepetition() {
+ $route = new Route(array('template' => '/{:id:[0-9a-f]{24}}.{:type}'));
+ $data = $route->export();
+ $this->assertEqual('@^(?:/(?P<id>[0-9a-f]{24}))\.(?P<type>[^\/]+)$@', $data['pattern']);
+
+ $this->assertEqual(array('id' => 'id', 'type' => 'type'), $data['keys']);
+ $this->assertEqual(array('id' => '[0-9a-f]{24}'), $data['subPatterns']);
+ }
+
+ /**
+ * Tests that route handlers are able to modify route parameters.
+ *
+ * @return void
+ */
+ public function testHandlerModification() {
+ $route = new Route(array(
+ 'template' => '/{:id:[0-9a-f]{24}}.{:type}',
+ 'handler' => function($request) {
+ $request->params += array('lang' => $request->env('ACCEPT_LANG') ?: 'en');
+ return $request;
+ }
+ ));
+
+ $request = new Request(array('url' => '/4bbf25bd8ead0e5180120000.json'));
+ $result = $route->parse($request);
+ $lang = $request->env('ACCEPT_LANG') ?: 'en';
+ $this->assertEqual($lang, $result->params['lang']);
+ }
+
+ /**
+ * Tests that requests can be routed based on HTTP method verbs or HTTP headers.
+ *
+ * @return void
+ */
+ public function testHeaderAndMethodBasedRouting() {
+ $parameters = array('controller' => 'users', 'action' => 'edit');
+
+ $route = new Route(array(
+ 'template' => '/',
+ 'params' => $parameters + array('http:method' => 'POST')
+ ));
+
+ $request = new Request(array('env' => array('HTTP_METHOD' => 'GET')));
+ $request->url = '/';
+ $this->assertFalse($route->parse($request));
+
+ $request = new Request(array('env' => array('REQUEST_METHOD' => 'POST')));
+ $request->url = '/';
+ $this->assertEqual($parameters, $route->parse($request)->params);
+
+ $route = new Route(array(
+ 'template' => '/{:controller}/{:id:[0-9]+}',
+ 'params' => $parameters + array('http:method' => array('POST', 'PUT'))
+ ));
+
+ $request = new Request(array('env' => array('REQUEST_METHOD' => 'PUT')));
+ $request->url = '/users/abc';
+ $this->assertFalse($route->parse($request));
+
+ $request->url = '/users/54';
+ $this->assertEqual($parameters + array('id' => '54'), $route->parse($request)->params);
+ }
+
+ /**
+ * Tests that a successful match against a route with template `'/'` operating at the root of
+ * a domain never returns an empty string.
+ *
+ * @return void
+ */
+ public function testMatchingEmptyRoute() {
+ $route = new Route(array(
+ 'template' => '/',
+ 'params' => array('controller' => 'users', 'action' => 'view')
+ ));
+
+ $request = new Request(array('base' => '/'));
+ $url = $route->match(array('controller' => 'users', 'action' => 'view'), $request);
+ $this->assertEqual('/', $url);
+
+ $request = new Request(array('base' => ''));
+ $url = $route->match(array('controller' => 'users', 'action' => 'view'), $request);
+ $this->assertEqual('/', $url);
+ }
+
+ /**
+ * Tests that routes with optional trailing elements have unnecessary slashes trimmed.
+ *
+ * @return void
+ */
+ public function testTrimmingEmptyPathElements() {
+ $route = new Route(array(
+ 'template' => '/{:controller}/{:id:[0-9]+}',
+ 'params' => array('action' => 'index', 'id' => null)
+ ));
+
+ $url = $route->match(array('controller' => 'posts', 'id' => '13'));
+ $this->assertEqual("/posts/13", $url);
+
+ $url = $route->match(array('controller' => 'posts'));
+ $this->assertEqual("/posts", $url);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/RouterTest.php b/libraries/lithium/tests/cases/net/http/RouterTest.php
index b67c7f4..57c2759 100644
--- a/libraries/lithium/tests/cases/net/http/RouterTest.php
+++ b/libraries/lithium/tests/cases/net/http/RouterTest.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\http;
-use \lithium\net\http\Route;
-use \lithium\net\http\Router;
-use \lithium\action\Request;
+use lithium\action\Request;
+use lithium\net\http\Route;
+use lithium\net\http\Router;
+use lithium\action\Response;
class RouterTest extends \lithium\test\Unit {
@@ -39,9 +40,12 @@ class RouterTest extends \lithium\test\Unit {
'pattern' => '@^/hello$@',
'params' => array('controller' => 'posts', 'action' => 'index'),
'match' => array('controller' => 'posts', 'action' => 'index'),
+ 'meta' => array(),
+ 'persist' => array('controller'),
'defaults' => array(),
'keys' => array(),
- 'subPatterns' => array()
+ 'subPatterns' => array(),
+ 'handler' => null
);
$this->assertEqual($expected, $result->export());
@@ -53,8 +57,11 @@ class RouterTest extends \lithium\test\Unit {
'params' => array('action' => 'view'),
'defaults' => array('action' => 'view'),
'match' => array(),
+ 'meta' => array(),
+ 'persist' => array('controller'),
'keys' => array('controller' => 'controller', 'action' => 'action'),
- 'subPatterns' => array()
+ 'subPatterns' => array(),
+ 'handler' => null
);
$this->assertEqual($expected, $result->export());
}
@@ -75,7 +82,10 @@ class RouterTest extends \lithium\test\Unit {
'params' => array('action' => 'view', 'required' => true),
'defaults' => array('action' => 'view'),
'match' => array('required' => true),
- 'subPatterns' => array()
+ 'meta' => array(),
+ 'persist' => array('controller'),
+ 'subPatterns' => array(),
+ 'handler' => null
);
$this->assertEqual($expected, $result->export());
}
@@ -88,8 +98,11 @@ class RouterTest extends \lithium\test\Unit {
'keys' => array('controller' => 'controller', 'action' => 'action'),
'params' => array('action' => 'archive'),
'match' => array(),
+ 'meta' => array(),
+ 'persist' => array('controller'),
'defaults' => array('action' => 'archive'),
- 'subPatterns' => array()
+ 'subPatterns' => array(),
+ 'handler' => null
);
$this->assertEqual($expected, $result->export());
}
@@ -106,7 +119,8 @@ class RouterTest extends \lithium\test\Unit {
foreach (array('/hello/', '/hello', 'hello/', 'hello') as $url) {
$this->request->url = $url;
$result = Router::parse($this->request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
+ $this->assertEqual(array('controller'), $result->persist);
}
}
@@ -117,14 +131,15 @@ class RouterTest extends \lithium\test\Unit {
foreach (array('/posts/view', '/posts', 'posts', 'posts/view', 'posts/view/') as $url) {
$this->request->url = $url;
$result = Router::parse($this->request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
+ $this->assertEqual(array('controller'), $result->persist);
}
$expected['action'] = 'index';
foreach (array('/posts/index', 'posts/index', 'posts/index/') as $url) {
$this->request->url = $url;
$result = Router::parse($this->request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
}
$this->request->url = '/posts/view/1';
@@ -139,7 +154,6 @@ class RouterTest extends \lithium\test\Unit {
*/
public function testStringActions() {
Router::connect('/login', array('controller' => 'sessions', 'action' => 'create'));
- Router::connect('/{:controller}', array('action' => 'index'));
Router::connect('/{:controller}/{:action}');
$result = Router::match("Sessions::create");
@@ -152,8 +166,113 @@ class RouterTest extends \lithium\test\Unit {
$this->assertEqual('/list_items/archive', $result);
}
+ public function testNamedAnchor() {
+ Router::connect('/{:controller}/{:action}');
+ Router::connect('/{:controller}/{:action}/{:id:[0-9]+}', array('id' => null));
+
+ $result = Router::match(array('Posts::edit', '#' => 'foo'));
+ $this->assertEqual('/posts/edit#foo', $result);
+
+ $result = Router::match(array('Posts::edit', 'id' => 42, '#' => 'foo'));
+ $this->assertEqual('/posts/edit/42#foo', $result);
+
+ $result = Router::match(array('controller' => 'users', 'action' => 'view', '#' => 'blah'));
+ $this->assertEqual('/users/view#blah', $result);
+
+ $result = Router::match(array(
+ 'controller' => 'users', 'action' => 'view', 'id' => 47, '#' => 'blargh'
+ ));
+ $this->assertEqual('/users/view/47#blargh', $result);
+ }
+
+ public function testQueryString() {
+ Router::connect('/{:controller}/{:action}');
+ Router::connect('/{:controller}/{:action}/{:id:[0-9]+}', array('id' => null));
+
+ $result = Router::match(array('Posts::edit', '?' => array('key' => 'value')));
+ $this->assertEqual('/posts/edit?key=value', $result);
+
+ $result = Router::match(array(
+ 'Posts::edit', 'id' => 42, '?' => array('key' => 'value', 'test' => 'foo')
+ ));
+ $this->assertEqual('/posts/edit/42?key=value&test=foo', $result);
+ }
+
/**
- * Tests that routing is fully reset when `Router::connect()` is passed a null value
+ * Tests that URLs specified as "Controller::action" and including additional parameters are
+ * interpreted properly.
+ *
+ * @return void
+ */
+ public function testEmbeddedStringActions() {
+ Router::connect('/logout/{:id:[0-9]{5,6}}', array(
+ 'controller' => 'sessions', 'action' => 'destroy', 'id' => null
+ ));
+ Router::connect('/{:controller}/{:action}');
+ Router::connect('/{:controller}/{:action}/{:id:[0-9]+}', array('id' => null));
+
+ $result = Router::match("Sessions::create");
+ $this->assertEqual('/sessions/create', $result);
+
+ $result = Router::match(array("Sessions::create"));
+ $this->assertEqual('/sessions/create', $result);
+
+ $result = Router::match(array("Sessions::destroy", 'id' => '03815'));
+ $this->assertEqual('/logout/03815', $result);
+
+ $result = Router::match("Posts::index");
+ $this->assertEqual('/posts', $result);
+
+ $ex = "No parameter match found for URL ";
+ $ex .= "`('controller' => 'sessions', 'action' => 'create', 'id' => 'foo')`.";
+ $this->expectException($ex);
+ $result = Router::match(array("Sessions::create", 'id' => 'foo'));
+ }
+
+ /**
+ * Tests that routes can be created with shorthand strings, i.e. `'Controller::action'` and
+ * `array('Controller::action', 'id' => '...')`.
+ *
+ * @return void
+ */
+ public function testStringParameterConnect() {
+ Router::connect('/posts/{:id:[0-9a-f]{24}}', 'Posts::edit');
+
+ $result = Router::match(array(
+ 'controller' => 'posts', 'action' => 'edit', 'id' => '4bbf25bd8ead0e5180130000'
+ ));
+ $expected = '/posts/4bbf25bd8ead0e5180130000';
+ $this->assertEqual($expected, $result);
+
+ $ex = "No parameter match found for URL `(";
+ $ex .= "'controller' => 'posts', 'action' => 'view', 'id' => '4bbf25bd8ead0e5180130000')`.";
+ $this->expectException($ex);
+
+ $result = Router::match(array(
+ 'controller' => 'posts', 'action' => 'view', 'id' => '4bbf25bd8ead0e5180130000'
+ ));
+ }
+
+ public function testShorthandParameterMatching() {
+ Router::reset();
+ Router::connect('/posts/{:page:[0-9]+}', array('Posts::index', 'page' => '1'));
+
+ $result = Router::match(array('controller' => 'posts', 'page' => '5'));
+ $expected = '/posts/5';
+ $this->assertEqual($expected, $result);
+
+ $result = Router::match(array('Posts::index', 'page' => '10'));
+ $expected = '/posts/10';
+ $this->assertEqual($expected, $result);
+
+ $request = new Request(array('url' => '/posts/13'));
+ $result = Router::process($request);
+ $expected = array('controller' => 'posts', 'action' => 'index', 'page' => '13');
+ $this->assertEqual($expected, $result->params);
+ }
+
+ /**
+ * Tests that routing is fully reset when calling `Router::reset()`.
*
* @return void
*/
@@ -163,7 +282,7 @@ class RouterTest extends \lithium\test\Unit {
$expected = array('controller' => 'hello', 'action' => 'index');
$result = Router::parse($this->request);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result->params);
Router::reset();
$this->assertNull(Router::parse($this->request));
@@ -178,7 +297,11 @@ class RouterTest extends \lithium\test\Unit {
Router::connect('/login', array('controller' => 'sessions', 'action' => 'add'));
$result = Router::match(array('controller' => 'sessions', 'action' => 'add'));
$this->assertEqual('/login', $result);
- $this->assertFalse(Router::match(array('controller' => 'sessions', 'action' => 'index')));
+
+ $this->expectException(
+ "No parameter match found for URL `('controller' => 'sessions', 'action' => 'index')`."
+ );
+ Router::match(array('controller' => 'sessions', 'action' => 'index'));
}
/**
@@ -190,8 +313,10 @@ class RouterTest extends \lithium\test\Unit {
Router::connect('/{:controller}');
$this->assertEqual('/posts', Router::match(array('controller' => 'posts')));
- $result = Router::match(array('controller' => 'posts', 'action' => 'view'));
- $this->assertFalse($result);
+ $this->expectException(
+ "No parameter match found for URL `('controller' => 'posts', 'action' => 'view')`."
+ );
+ Router::match(array('controller' => 'posts', 'action' => 'view'));
}
/**
@@ -215,8 +340,50 @@ class RouterTest extends \lithium\test\Unit {
$result = Router::match(array('controller' => 'posts', 'action' => 'view'));
$this->assertEqual('/posts/view', $result);
- $result = Router::match(array('controller' => 'posts', 'action' => 'view', 'id' => '2'));
- $this->assertFalse($result);
+ $ex = "No parameter match found for URL ";
+ $ex .= "`('controller' => 'posts', 'action' => 'view', 'id' => '2')`.";
+ $this->expectException($ex);
+ Router::match(array('controller' => 'posts', 'action' => 'view', 'id' => '2'));
+ }
+
+ /**
+ * Tests matching routes and returning an absolute (protocol + hostname) URL.
+ *
+ * @return void
+ */
+ public function testRouteMatchAbsoluteUrl() {
+ Router::connect('/login', array('controller' => 'sessions', 'action' => 'add'));
+ $result = Router::match('Sessions::add', $this->request);
+ $base = $this->request->env('base');
+ $this->assertEqual($base . '/login', $result);
+
+ $result = Router::match('Sessions::add', $this->request, array('absolute' => true));
+ $base = $this->request->env('HTTPS') ? 'https://' : 'http://';
+ $base .= $this->request->env('HTTP_HOST');
+ $base .= $this->request->env('base');
+ $this->assertEqual($base . '/login', $result);
+
+ $result = Router::match('Sessions::add',
+ $this->request, array('host' => 'test.local', 'absolute' => true)
+ );
+ $base = $this->request->env('HTTPS') ? 'https://' : 'http://';
+ $base .= 'test.local';
+ $base .= $this->request->env('base');
+ $this->assertEqual($base . '/login', $result);
+
+ $result = Router::match('Sessions::add',
+ $this->request, array('scheme' => 'https://', 'absolute' => true)
+ );
+ $base = 'https://' . $this->request->env('HTTP_HOST');
+ $base .= $this->request->env('base');
+ $this->assertEqual($base . '/login', $result);
+
+ $result = Router::match('Sessions::add',
+ $this->request, array('scheme' => 'https://', 'absolute' => true)
+ );
+ $base = 'https://' . $this->request->env('HTTP_HOST');
+ $base .= $this->request->env('base');
+ $this->assertEqual($base . '/login', $result);
}
/**
@@ -251,6 +418,13 @@ class RouterTest extends \lithium\test\Unit {
$expected = '/my/web/path/posts';
$this->assertEqual($expected, $result);
+ $request = new Request(array('base' => '/my/web/path'));
+ $result = Router::match('/some/where', $request, array('absolute' => true));
+ $prefix = $this->request->env('HTTPS') ? 'https://' : 'http://';
+ $prefix .= $this->request->env('HTTP_HOST');
+ $this->assertEqual($prefix . '/my/web/path/some/where', $result);
+
+
$result = Router::match('mailto:foo@localhost');
$expected = 'mailto:foo@localhost';
$this->assertEqual($expected, $result);
@@ -291,6 +465,190 @@ class RouterTest extends \lithium\test\Unit {
));
$this->assertEqual($expected, $result);
}
+
+ public function testProcess() {
+ Router::connect('/add/{:args}', array('controller' => 'tests', 'action' => 'add'));
+ $request = Router::process(new Request(array('url' => '/add/foo/bar')));
+
+ $params = array('controller' => 'tests', 'action' => 'add', 'args' => array('foo', 'bar'));
+ $this->assertEqual($params, $request->params);
+ $this->assertEqual(array('controller'), $request->persist);
+
+ $request = Router::process(new Request(array('url' => '/remove/foo/bar')));
+ $this->assertFalse($request->params);
+ }
+
+ /**
+ * Tests that a request context with persistent parameters generates URLs where those parameters
+ * are properly taken into account.
+ *
+ * @return void
+ */
+ public function testParameterPersistence() {
+ Router::connect('/{:controller}/{:action}/{:id:[0-9]+}', array(), array(
+ 'persist' => array('controller', 'id')
+ ));
+
+ // URLs generated with $request will now have the 'controller' and 'id'
+ // parameters copied to new URLs.
+ $request = Router::process(new Request(array('url' => 'posts/view/1138')));
+
+ $params = array('action' => 'edit');
+ $url = Router::match($params, $request); // Returns: '/posts/edit/1138'
+ $this->assertEqual($this->request->env('base') . '/posts/edit/1138', $url);
+
+ Router::connect(
+ '/add/{:args}',
+ array('controller' => 'tests', 'action' => 'add'),
+ array('persist' => array('controller', 'action'))
+ );
+ $request = Router::process(new Request(array('url' => '/add/foo/bar', 'base' => '')));
+ $path = Router::match(array('args' => array('baz', 'dib')), $request);
+ $this->assertEqual('/add/baz/dib', $path);
+ }
+
+ /**
+ * Tests that persistent parameters can be overridden with nulled-out values.
+ *
+ * @return void
+ */
+ public function testOverridingPersistentParameters() {
+ Router::connect(
+ '/admin/{:controller}/{:action}',
+ array('admin' => true),
+ array('persist' => array('admin', 'controller'))
+ );
+ Router::connect('/{:controller}/{:action}');
+
+ $request = Router::process(new Request(array('url' => '/admin/posts/add', 'base' => '')));
+ $expected = array('controller' => 'posts', 'action' => 'add', 'admin' => true);
+ $this->assertEqual($expected, $request->params);
+ $this->assertEqual(array('admin', 'controller'), $request->persist);
+
+ $url = Router::match(array('action' => 'archive'), $request);
+ $this->assertEqual('/admin/posts/archive', $url);
+
+ $url = Router::match(array('action' => 'archive', 'admin' => null), $request);
+ $this->assertEqual('/posts/archive', $url);
+ }
+
+ /**
+ * Tests passing a closure handler to `Router::connect()` to bypass or augment default
+ * dispatching.
+ *
+ * @return void
+ */
+ public function testRouteHandler() {
+ Router::connect('/login', 'Users::login');
+
+ Router::connect('/users/login', array(), function($request) {
+ return new Response(array(
+ 'location' => array('controller' => 'users', 'action' => 'login')
+ ));
+ });
+
+ $result = Router::process(new Request(array('url' => '/users/login')));
+ $this->assertTrue($result instanceof Response);
+
+ $headers = array('location' => '/login');
+ $this->assertEqual($headers, $result->headers);
+ }
+
+ /**
+ * Tests that a successful match against a route with template `'/'` operating at the root of
+ * a domain never returns an empty string.
+ *
+ * @return void
+ */
+ public function testMatchingEmptyRoute() {
+ Router::connect('/', 'Users::view');
+
+ $request = new Request(array('base' => '/'));
+ $url = Router::match(array('controller' => 'users', 'action' => 'view'), $request);
+ $this->assertEqual('/', $url);
+
+ $request = new Request(array('base' => ''));
+ $url = Router::match(array('controller' => 'users', 'action' => 'view'), $request);
+ $this->assertEqual('/', $url);
+ }
+
+ /**
+ * Tests routing based on content type extensions, with HTML being the default when types are
+ * not defined.
+ *
+ * @return void
+ */
+ public function testTypeBasedRouting() {
+ Router::connect('/{:controller}/{:id:[0-9]+}', array(
+ 'action' => 'index', 'type' => 'html', 'id' => null
+ ));
+ Router::connect('/{:controller}/{:id:[0-9]+}.{:type}', array(
+ 'action' => 'index', 'id' => null
+ ));
+
+ Router::connect('/{:controller}/{:action}/{:id:[0-9]+}', array(
+ 'type' => 'html', 'id' => null
+ ));
+ Router::connect('/{:controller}/{:action}/{:id:[0-9]+}.{:type}', array('id' => null));
+
+ $url = Router::match(array('controller' => 'posts', 'type' => 'html'));
+ $this->assertEqual('/posts', $url);
+
+ $url = Router::match(array('controller' => 'posts', 'type' => 'json'));
+ $this->assertEqual('/posts.json', $url);
+ }
+
+ /**
+ * Tests that routes can be connected and correctly match based on HTTP headers or method verbs.
+ *
+ * @return void
+ */
+ public function testHttpMethodBasedRouting() {
+ Router::connect('/{:controller}/{:id:[0-9]+}', array(
+ 'http:method' => 'GET', 'action' => 'view'
+ ));
+ Router::connect('/{:controller}/{:id:[0-9]+}', array(
+ 'http:method' => 'PUT', 'action' => 'edit'
+ ));
+
+ $request = new Request(array('url' => '/posts/13', 'env' => array(
+ 'REQUEST_METHOD' => 'GET'
+ )));
+ $params = Router::process($request)->params;
+ $expected = array('controller' => 'posts', 'action' => 'view', 'id' => '13');
+ $this->assertEqual($expected, $params);
+
+ $this->assertEqual('/posts/13', Router::match($params));
+
+ $request = new Request(array('url' => '/posts/13', 'env' => array(
+ 'REQUEST_METHOD' => 'PUT'
+ )));
+ $params = Router::process($request)->params;
+ $expected = array('controller' => 'posts', 'action' => 'edit', 'id' => '13');
+ $this->assertEqual($expected, $params);
+
+ $request = new Request(array('url' => '/posts/13', 'env' => array(
+ 'REQUEST_METHOD' => 'POST'
+ )));
+ $params = Router::process($request)->params;
+ $this->assertFalse($params);
+ }
+
+ /**
+ * Tests that the class dependency configuration can be modified.
+ *
+ * @return void
+ */
+ public function testCustomConfiguration() {
+ $old = Router::config();
+ $config = array('classes' => array('route' => 'my\custom\Route'));
+
+ Router::config($config);
+ $this->assertEqual($config, Router::config());
+
+ Router::config($old);
+ $this->assertEqual($old, Router::config());
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/http/ServiceTest.php b/libraries/lithium/tests/cases/net/http/ServiceTest.php
index e4eb379..b25ae69 100644
--- a/libraries/lithium/tests/cases/net/http/ServiceTest.php
+++ b/libraries/lithium/tests/cases/net/http/ServiceTest.php
@@ -2,79 +2,62 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\http;
-use \lithium\tests\mocks\net\http\MockService;
+use lithium\net\http\Media;
+use lithium\net\http\Service;
class ServiceTest extends \lithium\test\Unit {
public $request = null;
protected $_testConfig = array(
- 'classes' => array(
- 'socket' => '\lithium\tests\mocks\net\http\MockSocket'
- ),
- 'persistent' => false,
- 'protocol' => 'tcp',
+ 'classes' => array('response' => 'lithium\net\http\Response'),
+ 'socket' => 'lithium\tests\mocks\net\http\MockSocket',
'host' => 'localhost',
- 'login' => 'root',
- 'password' => '',
'port' => 80,
'timeout' => 2
);
+ public function setUp() {
+ Media::reset();
+ }
+
public function testAllMethodsNoConnection() {
- $http = new MockService(array('classes' => array('socket' => false)));
- $this->assertFalse($http->connect());
- $this->assertTrue($http->disconnect());
+ $http = new Service(array('socket' => false));
$this->assertFalse($http->get());
$this->assertFalse($http->post());
$this->assertFalse($http->put());
$this->assertFalse($http->delete());
}
- public function testConnect() {
- $http = new MockService($this->_testConfig);
- $result = $http->connect();
- $this->assertTrue($result);
- }
-
- public function testDisconnect() {
- $http = new MockService($this->_testConfig);
- $result = $http->connect();
- $this->assertTrue($result);
-
- $result = $http->disconnect();
- $this->assertTrue($result);
- }
-
- public function testPath() {
- $http = new MockService(array('host' => 'localhost') + $this->_testConfig);
+ public function testRequestPath() {
+ $http = new Service(array('host' => 'localhost') + $this->_testConfig);
$result = $http->get();
$expected = '/';
$result = $http->last->request->path;
$this->assertEqual($expected, $result);
- $http = new MockService(array('host' => 'localhost/base/path/') + $this->_testConfig);
+ $http = new Service(array('host' => 'localhost/base/path/') + $this->_testConfig);
$result = $http->get();
$expected = '/base/path/';
$result = $http->last->request->path;
$this->assertEqual($expected, $result);
- $http = new MockService(array('host' => 'localhost/base/path') + $this->_testConfig);
+ $http = new Service(array('host' => 'localhost/base/path') + $this->_testConfig);
$result = $http->get('/somewhere');
$expected = '/base/path/somewhere';
$result = $http->last->request->path;
$this->assertEqual($expected, $result);
- $http = new MockService(array('host' => 'localhost/base/path/') + $this->_testConfig);
+ $http = new Service(array('host' => 'localhost/base/path/') + $this->_testConfig);
$result = $http->get('/somewhere');
$expected = '/base/path/somewhere';
@@ -82,118 +65,161 @@ class ServiceTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
- public function testGet() {
- $http = new MockService($this->_testConfig);
- $result = $http->get();
- $this->assertEqual('Test!', $result);
-
- $expected = 'HTTP/1.1';
- $result = $http->last->response->protocol;
- $this->assertEqual($expected, $result);
-
- $expected = '200';
- $result = $http->last->response->status['code'];
- $this->assertEqual($expected, $result);
-
- $expected = 'OK';
- $result = $http->last->response->status['message'];
- $this->assertEqual($expected, $result);
-
- $expected = 'text/html';
- $result = $http->last->response->type;
- $this->assertEqual($expected, $result);
+ public function testHead() {
+ $http = new Service($this->_testConfig);
+ $this->assertEqual('', $http->head());
+ $this->assertEqual('HTTP/1.1', $http->last->response->protocol);
+ $this->assertEqual('200', $http->last->response->status['code']);
+ $this->assertEqual('OK', $http->last->response->status['message']);
+ $this->assertEqual('text/html', $http->last->response->type);
+ $this->assertEqual('UTF-8', $http->last->response->encoding);
+ $this->assertEqual('', $http->last->response->body());
+ }
- $expected = 'UTF-8';
- $result = $http->last->response->charset;
- $this->assertEqual($expected, $result);
+ public function testGet() {
+ $http = new Service($this->_testConfig);
+ $this->assertEqual('', $http->get());
+ $this->assertEqual('HTTP/1.1', $http->last->response->protocol);
+ $this->assertEqual('200', $http->last->response->status['code']);
+ $this->assertEqual('OK', $http->last->response->status['message']);
+ $this->assertEqual('text/html', $http->last->response->type);
+ $this->assertEqual('UTF-8', $http->last->response->encoding);
}
public function testGetPath() {
- $http = new MockService($this->_testConfig);
- $result = $http->get('search.json');
- $this->assertEqual('Test!', $result);
-
- $expected = 'HTTP/1.1';
- $result = $http->last->response->protocol;
- $this->assertEqual($expected, $result);
-
- $expected = '200';
- $result = $http->last->response->status['code'];
- $this->assertEqual($expected, $result);
-
- $expected = 'OK';
- $result = $http->last->response->status['message'];
- $this->assertEqual($expected, $result);
-
- $expected = 'text/html';
- $result = $http->last->response->type;
- $this->assertEqual($expected, $result);
-
- $expected = 'UTF-8';
- $result = $http->last->response->charset;
- $this->assertEqual($expected, $result);
+ $http = new Service($this->_testConfig);
+ $this->assertEqual('', $http->get('search.json'));
+ $this->assertEqual('HTTP/1.1', $http->last->response->protocol);
+ $this->assertEqual('200', $http->last->response->status['code']);
+ $this->assertEqual('OK', $http->last->response->status['message']);
+ $this->assertEqual('text/html', $http->last->response->type);
+ $this->assertEqual('UTF-8', $http->last->response->encoding);
}
public function testPost() {
- $http = new MockService($this->_testConfig);
+ $http = new Service($this->_testConfig);
$http->post('update.xml', array('status' => 'cool'));
$expected = join("\r\n", array(
'POST /update.xml HTTP/1.1',
'Host: localhost:80',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'Content-Type: application/x-www-form-urlencoded',
'Content-Length: 11',
'', 'status=cool'
));
$result = (string) $http->last->request;
$this->assertEqual($expected, $result);
+
+ $expected = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: 11',
+ '', 'status=cool'
+ ));
+ $result = (string) $http->last->response;
+ $this->assertEqual($expected, $result);
}
public function testPut() {
- $http = new MockService($this->_testConfig);
+ $http = new Service($this->_testConfig);
$http->put('update.xml', array('status' => 'cool'));
$expected = join("\r\n", array(
'PUT /update.xml HTTP/1.1',
'Host: localhost:80',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'Content-Type: application/x-www-form-urlencoded',
'Content-Length: 11',
'', 'status=cool'
));
$result = (string) $http->last->request;
$this->assertEqual($expected, $result);
+
+ $expected = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: 11',
+ '', 'status=cool'
+ ));
+ $result = (string) $http->last->response;
+ $this->assertEqual($expected, $result);
}
public function testDelete() {
- $http = new MockService($this->_testConfig);
+ $http = new Service($this->_testConfig);
$http->delete('posts/1');
$expected = join("\r\n", array(
'DELETE /posts/1 HTTP/1.1',
'Host: localhost:80',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'', ''
));
$result = (string) $http->last->request;
$this->assertEqual($expected, $result);
+
+ $expected = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ '', ''
+ ));
+ $result = (string) $http->last->response;
+ $this->assertEqual($expected, $result);
}
public function testJsonPost() {
- $http = new MockService($this->_testConfig);
+ $http = new Service($this->_testConfig);
$http->post('update.xml', array('status' => 'cool'), array('type' => 'json'));
$expected = join("\r\n", array(
'POST /update.xml HTTP/1.1',
'Host: localhost:80',
'Connection: Close',
- 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'User-Agent: Mozilla/5.0',
'Content-Type: application/json',
'Content-Length: 17',
'', '{"status":"cool"}'
));
$result = (string) $http->last->request;
$this->assertEqual($expected, $result);
+
+ $expected = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0',
+ 'Content-Type: application/json',
+ 'Content-Length: 17',
+ '', '{"status":"cool"}'
+ ));
+ $result = (string) $http->last->response;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testConnection() {
+ $http = new Service($this->_testConfig);
+ $connection = $http->connection();
+ $this->assertEqual('lithium\tests\mocks\net\http\MockSocket', get_class($connection));
+
+ $connection = $http->connection(array('scheme' => 'https'));
+ $config = $connection->config();
+ $this->assertEqual('https', $config['scheme']);
+ }
+
+ public function testSendConfiguringConnection() {
+ $http = new Service($this->_testConfig);
+ $result = $http->send('get', 'some-path/stuff', array(), array('someKey' => 'someValue'));
+ $config = array_pop($http->connection->configs);
+ $this->assertEqual('someValue', $config['someKey']);
+
}
}
diff --git a/libraries/lithium/tests/cases/net/socket/ContextTest.php b/libraries/lithium/tests/cases/net/socket/ContextTest.php
index 84f7b1c..fd1e989 100644
--- a/libraries/lithium/tests/cases/net/socket/ContextTest.php
+++ b/libraries/lithium/tests/cases/net/socket/ContextTest.php
@@ -2,13 +2,122 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\socket;
+use lithium\net\http\Request;
+use lithium\net\http\Response;
+use lithium\net\socket\Context;
+
class ContextTest extends \lithium\test\Unit {
+
+ protected $_testConfig = array(
+ 'persistent' => false,
+ 'scheme' => 'http',
+ 'host' => 'example.org',
+ 'port' => 80,
+ 'timeout' => 4,
+ 'classes' => array('request' => 'lithium\net\http\Request')
+ );
+
+ protected $_testUrl = 'http://example.org';
+
+ public function skip() {
+ $this->skipIf(dns_check_record("example.org") === false, "No internet connection.");
+ }
+
+ public function tearDown() {
+ unset($this->socket);
+ }
+
+ public function testConstruct() {
+ $subject = new Context(array('timeout' => 300));
+ $this->assertTrue(300, $subject->timeout());
+ unset($subject);
+ }
+
+ public function testGetSetTimeout() {
+ $this->assertEqual(4, $this->socket->timeout());
+ $this->assertEqual(25, $this->socket->timeout(25));
+ $this->assertEqual(25, $this->socket->timeout());
+
+ $this->socket->open();
+ $this->assertEqual(25, $this->socket->timeout());
+
+ $result = stream_context_get_options($this->socket->resource());
+ $this->assertEqual(25, $result['http']['timeout']);
+ }
+
+ public function testOpen() {
+ $stream = new Context($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ }
+
+ public function testClose() {
+ $stream = new Context($this->_testConfig);
+ $this->assertEqual(true, $stream->close());
+ }
+
+ public function testEncoding() {
+ $stream = new Context($this->_testConfig);
+ $this->assertEqual(false, $stream->encoding());
+ }
+
+ public function testEof() {
+ $stream = new Context($this->_testConfig);
+ $this->assertTrue(true, $stream->eof());
+ }
+
+ public function testMessageInConfig() {
+ $socket = new Context(array('message' => new Request()));
+ $this->assertTrue(is_resource($socket->open()));
+ }
+
+ public function testWriteAndRead() {
+ $stream = new Context($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $this->assertTrue(is_resource($stream->resource()));
+ $this->assertEqual(1, $stream->write());
+ $this->assertPattern("/^HTTP/", (string) $stream->read());
+ }
+
+ public function testSendWithNull() {
+ $stream = new Context($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send(
+ new Request($this->_testConfig),
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ $this->assertTrue($stream->eof());
+ }
+
+ public function testSendWithArray() {
+ $stream = new Context($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send($this->_testConfig,
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ $this->assertTrue($stream->eof());
+ }
+
+ public function testSendWithObject() {
+ $stream = new Context($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send(
+ new Request($this->_testConfig),
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ $this->assertTrue($stream->eof());
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/socket/CurlTest.php b/libraries/lithium/tests/cases/net/socket/CurlTest.php
index 67d5c87..49ef98e 100644
--- a/libraries/lithium/tests/cases/net/socket/CurlTest.php
+++ b/libraries/lithium/tests/cases/net/socket/CurlTest.php
@@ -2,39 +2,47 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\socket;
-use \lithium\tests\mocks\net\socket\MockCurl;
+use lithium\net\http\Request;
+use lithium\net\socket\Curl;
class CurlTest extends \lithium\test\Unit {
protected $_testConfig = array(
'persistent' => false,
- 'protocol' => 'tcp',
- 'host' => 'localhost',
- 'login' => 'root',
- 'password' => '',
+ 'scheme' => 'http',
+ 'host' => 'example.org',
'port' => 80,
- 'timeout' => 2
+ 'timeout' => 2,
+ 'classes' => array('request' => 'lithium\net\http\Request')
);
+ protected $_testUrl = 'http://example.org';
+
/**
* Skip the test if curl is not available in your PHP installation.
*
* @return void
*/
public function skip() {
- $extensionExists = function_exists('curl_init');
$message = 'Your PHP installation was not compiled with curl support.';
- $this->skipIf(!$extensionExists, $message);
+ $this->skipIf(!function_exists('curl_init'), $message);
+
+ $config = $this->_testConfig;
+ $url = "{$config['scheme']}://{$config['host']}";
+ $message = "Could not open {$url} - skipping " . __CLASS__;
+ $this->skipIf(!curl_init($url), $message);
+
+ $this->skipIf(dns_check_record("example.org") === false, "No internet connection.");
}
public function testAllMethodsNoConnection() {
- $stream = new MockCurl(array('protocol' => null));
+ $stream = new Curl(array('scheme' => null));
$this->assertFalse($stream->open());
$this->assertTrue($stream->close());
$this->assertFalse($stream->timeout(2));
@@ -44,7 +52,7 @@ class CurlTest extends \lithium\test\Unit {
}
public function testOpen() {
- $stream = new MockCurl($this->_testConfig);
+ $stream = new Curl($this->_testConfig);
$result = $stream->open();
$this->assertTrue($result);
@@ -53,7 +61,7 @@ class CurlTest extends \lithium\test\Unit {
}
public function testClose() {
- $stream = new MockCurl($this->_testConfig);
+ $stream = new Curl($this->_testConfig);
$result = $stream->open();
$this->assertTrue($result);
@@ -65,7 +73,7 @@ class CurlTest extends \lithium\test\Unit {
}
public function testTimeout() {
- $stream = new MockCurl($this->_testConfig);
+ $stream = new Curl($this->_testConfig);
$result = $stream->open();
$stream->timeout(10);
$result = $stream->resource();
@@ -73,34 +81,57 @@ class CurlTest extends \lithium\test\Unit {
}
public function testEncoding() {
- $stream = new MockCurl($this->_testConfig);
+ $stream = new Curl($this->_testConfig);
$result = $stream->open();
$stream->encoding('UTF-8');
$result = $stream->resource();
$this->assertTrue(is_resource($result));
- $stream = new MockCurl($this->_testConfig + array('encoding' => 'UTF-8'));
+ $stream = new Curl($this->_testConfig + array('encoding' => 'UTF-8'));
$result = $stream->open();
$result = $stream->resource();
$this->assertTrue(is_resource($result));
}
public function testWriteAndRead() {
- $stream = new MockCurl($this->_testConfig);
- $result = $stream->open();
- $this->assertTrue(is_resource($result));
-
- $result = $stream->resource();
- $this->assertTrue(is_resource($result));
+ $stream = new Curl($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $this->assertTrue(is_resource($stream->resource()));
+ $this->assertEqual(1, $stream->write());
+ $this->assertPattern("/^HTTP/", (string) $stream->read());
+ }
- $url = 'http://localhost';
+ public function testSendWithNull() {
+ $stream = new Curl($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send(
+ new Request($this->_testConfig),
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof \lithium\net\http\Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ }
- $stream->set(CURLOPT_URL, $url);
- $this->assertTrue($stream->write(null));
+ public function testSendWithArray() {
+ $stream = new Curl($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send($this->_testConfig,
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof \lithium\net\http\Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ }
- $result = $stream->read();
- $this->assertEqual(file_get_contents($url), $result);
+ public function testSendWithObject() {
+ $stream = new Curl($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send(
+ new Request($this->_testConfig),
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof \lithium\net\http\Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
}
}
-?>
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/net/socket/StreamTest.php b/libraries/lithium/tests/cases/net/socket/StreamTest.php
index 3518cde..a7bf98b 100644
--- a/libraries/lithium/tests/cases/net/socket/StreamTest.php
+++ b/libraries/lithium/tests/cases/net/socket/StreamTest.php
@@ -2,28 +2,34 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\net\socket;
-use \lithium\tests\mocks\net\socket\MockStream;
+use lithium\net\http\Request;
+use lithium\net\http\Response;
+use lithium\net\socket\Stream;
class StreamTest extends \lithium\test\Unit {
protected $_testConfig = array(
'persistent' => false,
- 'protocol' => 'tcp',
- 'host' => 'localhost',
- 'login' => 'root',
- 'password' => '',
+ 'scheme' => 'http',
+ 'host' => 'example.org',
'port' => 80,
- 'timeout' => 2
+ 'timeout' => 2,
+ 'classes' => array('request' => 'lithium\net\http\Request')
);
+ public function skip() {
+ $host = $this->_testConfig['host'];
+ $this->skipIf(dns_check_record($host) === false, "No internet connection.");
+ }
+
public function testAllMethodsNoConnection() {
- $stream = new MockStream(array('protocol' => null));
+ $stream = new Stream(array('scheme' => null));
$this->assertFalse($stream->open());
$this->assertTrue($stream->close());
$this->assertFalse($stream->timeout(2));
@@ -31,11 +37,11 @@ class StreamTest extends \lithium\test\Unit {
$this->assertFalse($stream->write(null));
$this->assertFalse($stream->read());
$this->assertTrue($stream->eof());
- $this->assertNull($stream->send(''));
+ $this->assertNull($stream->send(new Request()));
}
public function testOpen() {
- $stream = new MockStream($this->_testConfig);
+ $stream = new Stream($this->_testConfig);
$result = $stream->open();
$this->assertTrue($result);
@@ -44,7 +50,7 @@ class StreamTest extends \lithium\test\Unit {
}
public function testClose() {
- $stream = new MockStream($this->_testConfig);
+ $stream = new Stream($this->_testConfig);
$result = $stream->open();
$this->assertTrue($result);
@@ -56,7 +62,7 @@ class StreamTest extends \lithium\test\Unit {
}
public function testTimeout() {
- $stream = new MockStream($this->_testConfig);
+ $stream = new Stream($this->_testConfig);
$result = $stream->open();
$stream->timeout(10);
$result = $stream->resource();
@@ -64,44 +70,61 @@ class StreamTest extends \lithium\test\Unit {
}
public function testEncoding() {
- $stream = new MockStream($this->_testConfig);
+ $stream = new Stream($this->_testConfig);
$result = $stream->open();
$stream->encoding('UTF-8');
$result = $stream->resource();
$this->assertTrue(is_resource($result));
- $stream = new MockStream($this->_testConfig + array('encoding' => 'UTF-8'));
+ $stream = new Stream($this->_testConfig + array('encoding' => 'UTF-8'));
$result = $stream->open();
$result = $stream->resource();
$this->assertTrue(is_resource($result));
}
public function testWriteAndRead() {
- $stream = new MockStream($this->_testConfig);
- $result = $stream->open();
- $data = "GET / HTTP/1.1\r\n";
- $data .= "Host: localhost\r\n";
- $data .= "Connection: Close\r\n\r\n";
- $this->assertTrue($stream->write($data));
+ $stream = new Stream($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $this->assertTrue(is_resource($stream->resource()));
+
+ $result = $stream->write();
+ $this->assertTrue($result == 82 || $result == 84);
+ $this->assertPattern("/^HTTP/", (string) $stream->read());
+ }
- $result = $stream->eof();
- $this->assertFalse($result);
+ public function testSendWithNull() {
+ $stream = new Stream($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send(
+ new Request($this->_testConfig),
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ $this->assertTrue($stream->eof());
+ }
- $result = $stream->read();
- $this->assertPattern("/^HTTP/", $result);
+ public function testSendWithArray() {
+ $stream = new Stream($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send($this->_testConfig,
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ $this->assertTrue($stream->eof());
}
- public function testSend() {
- $stream = new MockStream($this->_testConfig);
- $result = $stream->open();
- $data = "GET / HTTP/1.1\r\n";
- $data .= "Host: localhost\r\n";
- $data .= "Connection: Close\r\n\r\n";
-
- $result = $stream->send($data, array('classes' => array(
- 'response' => '\lithium\net\http\Response'
- )));
- $this->assertNotEqual(null, $result);
+ public function testSendWithObject() {
+ $stream = new Stream($this->_testConfig);
+ $this->assertTrue(is_resource($stream->open()));
+ $result = $stream->send(
+ new Request($this->_testConfig),
+ array('response' => 'lithium\net\http\Response')
+ );
+ $this->assertTrue($result instanceof Response);
+ $this->assertPattern("/^HTTP/", (string) $result);
+ $this->assertTrue($stream->eof());
}
}
diff --git a/libraries/lithium/tests/cases/security/AuthTest.php b/libraries/lithium/tests/cases/security/AuthTest.php
index defac7e..32eac7b 100644
--- a/libraries/lithium/tests/cases/security/AuthTest.php
+++ b/libraries/lithium/tests/cases/security/AuthTest.php
@@ -8,8 +8,8 @@
namespace lithium\tests\cases\security;
-use \lithium\security\Auth;
-use \lithium\storage\Session;
+use lithium\security\Auth;
+use lithium\storage\Session;
class AuthTest extends \lithium\test\Unit {
@@ -20,7 +20,7 @@ class AuthTest extends \lithium\test\Unit {
Auth::config(array(
'test' => array(
- 'adapter' => '\lithium\tests\mocks\security\auth\adapter\MockAuthAdapter'
+ 'adapter' => 'lithium\tests\mocks\security\auth\adapter\MockAuthAdapter'
)
));
}
@@ -69,6 +69,13 @@ class AuthTest extends \lithium\test\Unit {
$this->assertFalse(Auth::set('test', $user, array('fail' => true)));
$this->assertFalse(Auth::check('test'));
}
+
+ public function testNoConfigurations() {
+ Auth::reset();
+ $this->assertIdentical(array(), Auth::config());
+ $this->expectException("Configuration `user` has not been defined.");
+ Auth::check('user');
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/security/auth/adapter/FormTest.php b/libraries/lithium/tests/cases/security/auth/adapter/FormTest.php
index 6e13eb8..eac9e72 100644
--- a/libraries/lithium/tests/cases/security/auth/adapter/FormTest.php
+++ b/libraries/lithium/tests/cases/security/auth/adapter/FormTest.php
@@ -8,9 +8,9 @@
namespace lithium\tests\cases\security\auth\adapter;
-use \lithium\action\Request;
-use \lithium\data\model\Record;
-use \lithium\security\auth\adapter\Form;
+use lithium\action\Request;
+use lithium\data\entity\Record;
+use lithium\security\auth\adapter\Form;
class FormTest extends \lithium\test\Unit {
@@ -24,20 +24,34 @@ class FormTest extends \lithium\test\Unit {
$request->data = array('username' => 'Person', 'password' => 'password');
$result = $subject->check($request);
- $expected = array('username' => 'Person', 'password' => sha1('password'));
+ $password = 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7';
+ $password .= '785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86';
+ $expected = array('username' => 'Person') + compact('password');
$this->assertEqual($expected, $result);
}
public function testLoginWithFilters() {
- $subject = new Form(array('model' => __CLASS__, 'filters' => array(
- 'username' => 'sha1'
- )));
+ $subject = new Form(array('model' => __CLASS__, 'filters' => array('username' => 'sha1')));
$request = new Request();
$request->data = array('username' => 'Person', 'password' => 'password');
+ $password = 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7';
+ $password .= '785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86';
+ $expected = array('username' => sha1('Person')) + compact('password');
+ $this->assertEqual($expected, $subject->check($request));
+ }
+
+ /**
+ * Tests that attempted exploitation via malformed credential submission.
+ *
+ * @return void
+ */
+ public function testLoginWithArray() {
+ $subject = new Form(array('model' => __CLASS__));
+ $request = new Request();
+ $request->data = array('username' => array('!=' => ''), 'password' => '');
$result = $subject->check($request);
- $expected = array('username' => sha1('Person'), 'password' => sha1('password'));
- $this->assertEqual($expected, $result);
+ $this->assertEqual('Array', $result['username']);
}
/**
diff --git a/libraries/lithium/tests/cases/security/auth/adapter/HttpTest.php b/libraries/lithium/tests/cases/security/auth/adapter/HttpTest.php
new file mode 100644
index 0000000..69c0de8
--- /dev/null
+++ b/libraries/lithium/tests/cases/security/auth/adapter/HttpTest.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\security\auth\adapter;
+
+use lithium\tests\mocks\security\auth\adapter\MockHttp;
+use lithium\action\Request;
+
+class HttpTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->request = new Request();
+ }
+
+ public function tearDown() {}
+
+ public function testCheckBasicIsFalse() {
+ $http = new MockHttp(array('method' => 'basic', 'users' => array('gwoo' => 'li3')));
+ $result = $http->check($this->request);
+ $this->assertFalse($result);
+
+ $expected = array('WWW-Authenticate: Basic realm="' . basename(LITHIUM_APP_PATH) . '"');
+ $result = $http->headers;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCheckBasicIsTrue() {
+ $request = new Request(array(
+ 'env' => array('PHP_AUTH_USER' => 'gwoo', 'PHP_AUTH_PW' => 'li3')
+ ));
+ $http = new MockHttp(array('method' => 'basic', 'users' => array('gwoo' => 'li3')));
+ $result = $http->check($request);
+ $this->assertTrue($result);
+
+ $expected = array();
+ $result = $http->headers;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCheckDigestIsFalse() {
+ $http = new MockHttp(array('users' => array('gwoo' => 'li3')));
+ $result = $http->check($this->request);
+ $this->assertFalse($result);
+
+ $this->assertPattern('/Digest/', $http->headers[0]);
+ $this->assertPattern('/qop="auth"/', $http->headers[0]);
+ $this->assertPattern('/nonce=/', $http->headers[0]);
+ }
+
+ public function testCheckDigestIsTrue() {
+ $request = new Request(array(
+ 'env' => array('PHP_AUTH_DIGEST' =>
+ 'qop="auth",nonce="4bca0fbca7bd0",'
+ . 'nc="00000001",cnonce="95b2cd1e179bf5414e52ed62811481cf",'
+ . 'uri="/http_auth",realm="Protected by Lithium",'
+ . 'opaque="d3fb67a7aa4d887ec4bf83040a820a46",username="gwoo",'
+ . 'response="04d7d878c67f289f37e553d2025e3a52"')
+ ));
+ $http = new MockHttp(array('users' => array('gwoo' => 'li3')));
+ $result = $http->check($request);
+ $this->assertTrue($result);
+
+ $expected = array();
+ $result = $http->headers;
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/CacheTest.php b/libraries/lithium/tests/cases/storage/CacheTest.php
index 002cef9..5744026 100644
--- a/libraries/lithium/tests/cases/storage/CacheTest.php
+++ b/libraries/lithium/tests/cases/storage/CacheTest.php
@@ -2,15 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage;
-use \lithium\storage\Cache;
-use \lithium\util\Collection;
-use \SplFileInfo;
+use SplFileInfo;
+use lithium\core\Libraries;
+use lithium\storage\Cache;
+use lithium\util\Collection;
class CacheTest extends \lithium\test\Unit {
@@ -191,6 +192,73 @@ class CacheTest extends \lithium\test\Unit {
$this->assertEqual($data, $result);
}
+ public function testCacheReadWithConditions() {
+ $config = array('default' => array(
+ 'adapter' => 'Memory', 'filters' => array()
+ ));
+ Cache::config($config);
+ $result = Cache::config();
+ $expected = $config;
+ $this->assertEqual($expected, $result);
+
+ $conditions = function() {
+ return false;
+ };
+
+ $result = Cache::read('default', 'some_key', compact('conditions'));
+ $this->assertFalse($result);
+
+ $conditions = function() use (&$config) {
+ return (isset($config['default']));
+ };
+
+ Cache::write('default', 'some_key', 'some value', '+1 minute');
+ $result = Cache::read('default', 'some_key', compact('conditions'));
+ $this->assertTrue($result);
+
+ $result = Cache::read('non_existing', 'key_value', compact('conditions'));
+ $this->assertFalse($result);
+ }
+
+ public function testCacheIncrementDecrementWithConditions() {
+ $config = array('default' => array(
+ 'adapter' => 'Memory', 'filters' => array()
+ ));
+ Cache::config($config);
+ $result = Cache::config();
+ $expected = $config;
+ $this->assertEqual($expected, $result);
+
+ $conditions = function() {
+ return false;
+ };
+
+ $result = Cache::increment('default', 'some_key', 1, compact('conditions'));
+ $this->assertFalse($result);
+
+ $conditions = function() use (&$config) {
+ return (isset($config['default']));
+ };
+
+ Cache::write('default', 'some_key', 1, '+1 minute');
+ $result = Cache::increment('default', 'some_key', 1, compact('conditions'));
+ $this->assertEqual(2, $result);
+
+ $conditions = function() {
+ return false;
+ };
+
+ $result = Cache::decrement('default', 'decrement_some_key', 1, compact('conditions'));
+ $this->assertFalse($result);
+
+ $conditions = function() use (&$config) {
+ return (isset($config['default']));
+ };
+ Cache::write('default', 'decrement_some_key', 1, '+1 minute');
+ $result = Cache::decrement('default', 'decrement_some_key', 1, compact('conditions'));
+ $this->assertEqual(0, $result);
+ }
+
public function testCacheWriteWithConditions() {
$config = array('default' => array(
'adapter' => 'Memory', 'filters' => array()
@@ -200,20 +268,53 @@ class CacheTest extends \lithium\test\Unit {
$expected = $config;
$this->assertEqual($expected, $result);
- $result = Cache::write('default', 'some_key', 'some_data', '+1 minute', function() {
+ $conditions = function() {
return false;
- });
+ };
+ $result = Cache::write(
+ 'default', 'some_key', 'some_data', '+1 minute', compact('conditions')
+ );
$this->assertFalse($result);
- $anonymous = function() use (&$config) {
+ $conditions = function() use (&$config) {
return (isset($config['default']));
};
- $result = Cache::write('default', 'some_key', 'some_data', '+1 minute', $anonymous);
+
+ $result = Cache::write(
+ 'default', 'some_key', 'some_data', '+1 minute', compact('conditions')
+ );
$this->assertTrue($result);
- $result = Cache::write('non_existing', 'key_value', 'data', '+1 minute', $anonymous);
+ $result = Cache::write(
+ 'non_existing', 'key_value', 'data', '+1 minute', compact('conditions')
+ );
$this->assertFalse($result);
+ }
+ public function testCacheReadThroughWrite() {
+ $config = array('default' => array(
+ 'adapter' => 'Memory', 'filters' => array()
+ ));
+ Cache::config($config);
+ $result = Cache::config();
+ $expected = $config;
+ $this->assertEqual($expected, $result);
+
+ $write = function() {
+ return array('+1 minute' => 'read-through write');
+ };
+ $result = Cache::read('default', 'read_through', compact('write'));
+ $this->assertEqual('read-through write', $result);
+
+ $result = Cache::read('default', 'read_through');
+ $this->assertEqual('read-through write', $result);
+
+ $write = array('+1 minute' => 'string read-through write');
+ $result = Cache::read('default', 'string_read_through', compact('write'));
+ $this->assertEqual('string read-through write', $result);
+
+ $result = Cache::read('default', 'string_read_through');
+ $this->assertEqual('string read-through write', $result);
}
public function testCacheReadAndWrite() {
@@ -261,21 +362,22 @@ class CacheTest extends \lithium\test\Unit {
$expected = $config;
$this->assertEqual($expected, $result);
- $anonymous = function() use (&$config) {
+ $conditions = function() use (&$config) {
return (isset($config['default']));
};
- $result = Cache::read('non_existing', 'key_value', $anonymous);
+ $result = Cache::read('non_existing', 'key_value', compact('conditions'));
$this->assertFalse($result);
- $result = Cache::read('default', 'key_value', $anonymous);
+ $result = Cache::read('default', 'key_value', compact('conditions'));
$this->assertFalse($result);
- $result = Cache::write('default', 'keyed', 'some data', '+1 minute', $anonymous);
+ $result = Cache::write('default', 'keyed', 'some data', '+1 minute', compact('conditions'));
$this->assertTrue($result);
- $result = Cache::write('default', 'keyed', 'some data', '+1 minute', function() {
+ $conditions = function() {
return false;
- });
+ };
+ $result = Cache::write('default', 'keyed', 'some data', '+1 minute', compact('conditions'));
$this->assertFalse($result);
}
@@ -308,19 +410,23 @@ class CacheTest extends \lithium\test\Unit {
$expected = $config;
$this->assertEqual($expected, $result);
- $anonymous = function() use (&$config) {
+ $conditions = function() use (&$config) {
return (isset($config['default']));
};
- $result = Cache::delete('non_existing', 'key_value', $anonymous);
+ $result = Cache::delete('non_existing', 'key_value', compact('conditions'));
$this->assertFalse($result);
$result = Cache::write('default', 'to delete', 'dead data', '+1 minute');
$this->assertTrue($result);
- $result = Cache::delete('default', 'to delete', function() { return false; });
+ $result = Cache::delete('default', 'to delete', array(
+ 'conditions' => function() {
+ return false;
+ }
+ ));
$this->assertFalse($result);
- $result = Cache::delete('default', 'to delete', $anonymous);
+ $result = Cache::delete('default', 'to delete', compact('conditions'));
$this->assertTrue($result);
}
@@ -389,6 +495,9 @@ class CacheTest extends \lithium\test\Unit {
$expected = $config;
$this->assertEqual($expected, $result);
+ $result = Cache::increment('does_not_exist', 'inc');
+ $this->assertFalse($result);
+
$result = Cache::write('default', 'increment', 5, '+1 minute');
$this->assertTrue($result);
@@ -408,6 +517,9 @@ class CacheTest extends \lithium\test\Unit {
$expected = $config;
$this->assertEqual($expected, $result);
+ $result = Cache::decrement('does_not_exist', 'dec');
+ $this->assertFalse($result);
+
$result = Cache::write('default', 'decrement', 5, '+1 minute');
$this->assertTrue($result);
@@ -442,14 +554,14 @@ class CacheTest extends \lithium\test\Unit {
}
public function testIntegrationFileAdapterWrite() {
- $directory = new SplFileInfo(LITHIUM_APP_PATH . "/resources/tmp/cache/");
+ $directory = new SplFileInfo(Libraries::get(true, 'resources') . "/tmp/cache/");
$accessible = ($directory->isDir() && $directory->isReadable() && $directory->isWritable());
$message = "$directory does not have the proper permissions.";
$this->skipIf(!$accessible, $message);
$config = array('default' => array(
'adapter' => 'File',
- 'path' => LITHIUM_APP_PATH . '/resources/tmp/cache',
+ 'path' => Libraries::get(true, 'resources') . '/tmp/cache',
'filters' => array()
));
Cache::config($config);
@@ -458,24 +570,24 @@ class CacheTest extends \lithium\test\Unit {
$this->assertTrue($result);
$time = time() + 60;
- $result = file_get_contents(LITHIUM_APP_PATH . '/resources/tmp/cache/key');
+ $result = file_get_contents(Libraries::get(true, 'resources') . '/tmp/cache/key');
$expected = "{:expiry:$time}\nvalue";
$this->assertEqual($result, $expected);
- $result = unlink(LITHIUM_APP_PATH . '/resources/tmp/cache/key');
+ $result = unlink(Libraries::get(true, 'resources') . '/tmp/cache/key');
$this->assertTrue($result);
- $this->assertFalse(file_exists(LITHIUM_APP_PATH . '/resources/tmp/cache/key'));
+ $this->assertFalse(file_exists(Libraries::get(true, 'resources') . '/tmp/cache/key'));
}
public function testIntegrationFileAdapterWithStrategies() {
- $directory = new SplFileInfo(LITHIUM_APP_PATH . "/resources/tmp/cache/");
+ $directory = new SplFileInfo(Libraries::get(true, 'resources') . "/tmp/cache/");
$accessible = ($directory->isDir() && $directory->isReadable() && $directory->isWritable());
$message = "$directory does not have the proper permissions.";
$this->skipIf(!$accessible, $message);
$config = array('default' => array(
'adapter' => 'File',
- 'path' => LITHIUM_APP_PATH . '/resources/tmp/cache',
+ 'path' => Libraries::get(true, 'resources') . '/tmp/cache',
'filters' => array(),
'strategies' => array('Serializer')
));
@@ -486,7 +598,7 @@ class CacheTest extends \lithium\test\Unit {
$this->assertTrue($result);
$time = time() + 60;
- $result = file_get_contents(LITHIUM_APP_PATH . '/resources/tmp/cache/key');
+ $result = file_get_contents(Libraries::get(true, 'resources') . '/tmp/cache/key');
$expected = "{:expiry:$time}\na:1:{s:4:\"some\";s:4:\"data\";}";
$this->assertEqual($result, $expected);
@@ -494,20 +606,20 @@ class CacheTest extends \lithium\test\Unit {
$result = Cache::read('default', 'key');
$this->assertEqual($data, $result);
- $result = unlink(LITHIUM_APP_PATH . '/resources/tmp/cache/key');
+ $result = unlink(Libraries::get(true, 'resources') . '/tmp/cache/key');
$this->assertTrue($result);
- $this->assertFalse(file_exists(LITHIUM_APP_PATH . '/resources/tmp/cache/key'));
+ $this->assertFalse(file_exists(Libraries::get(true, 'resources') . '/tmp/cache/key'));
}
public function testIntegrationFileAdapterMultipleStrategies() {
- $directory = new SplFileInfo(LITHIUM_APP_PATH . "/resources/tmp/cache/");
+ $directory = new SplFileInfo(Libraries::get(true, 'resources') . "/tmp/cache/");
$accessible = ($directory->isDir() && $directory->isReadable() && $directory->isWritable());
$message = "$directory does not have the proper permissions.";
$this->skipIf(!$accessible, $message);
$config = array('default' => array(
'adapter' => 'File',
- 'path' => LITHIUM_APP_PATH . '/resources/tmp/cache',
+ 'path' => Libraries::get(true, 'resources') . '/tmp/cache',
'filters' => array(),
'strategies' => array('Serializer', 'Base64')
));
@@ -518,7 +630,7 @@ class CacheTest extends \lithium\test\Unit {
$this->assertTrue($result);
$time = time() + 60;
- $result = file_get_contents(LITHIUM_APP_PATH . '/resources/tmp/cache/key');
+ $result = file_get_contents(Libraries::get(true, 'resources') . '/tmp/cache/key');
$expected = "{:expiry:$time}\nYToxOntzOjQ6InNvbWUiO3M6NDoiZGF0YSI7fQ==";
$this->assertEqual($result, $expected);
@@ -526,9 +638,9 @@ class CacheTest extends \lithium\test\Unit {
$result = Cache::read('default', 'key');
$this->assertEqual($data, $result);
- $result = unlink(LITHIUM_APP_PATH . '/resources/tmp/cache/key');
+ $result = unlink(Libraries::get(true, 'resources') . '/tmp/cache/key');
$this->assertTrue($result);
- $this->assertFalse(file_exists(LITHIUM_APP_PATH . '/resources/tmp/cache/key'));
+ $this->assertFalse(file_exists(Libraries::get(true, 'resources') . '/tmp/cache/key'));
}
}
diff --git a/libraries/lithium/tests/cases/storage/SessionTest.php b/libraries/lithium/tests/cases/storage/SessionTest.php
index 9a2c984..0fc5542 100644
--- a/libraries/lithium/tests/cases/storage/SessionTest.php
+++ b/libraries/lithium/tests/cases/storage/SessionTest.php
@@ -2,18 +2,22 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage;
-use \lithium\storage\Session;
-use \lithium\util\Collection;
-use \lithium\storage\session\adapter\Memory;
-use \lithium\tests\mocks\storage\session\adapter\SessionStorageConditional;
+use lithium\storage\Session;
+use lithium\util\Collection;
+use lithium\storage\session\adapter\Memory;
+use lithium\tests\mocks\storage\session\adapter\SessionStorageConditional;
+/**
+ *
+ * @todo Refactor this to get rid of the very integration-style tests.
+ */
class SessionTest extends \lithium\test\Unit {
public function setUp() {
@@ -115,44 +119,54 @@ class SessionTest extends \lithium\test\Unit {
Session::write('key1', 'value', array('name' => 'persistent'));
Session::write('key2', 'value', array('name' => 'temp'));
- $result = Session::check('key1');
- $this->assertTrue($result);
-
- $result = Session::check('key2');
- $this->assertTrue($result);
+ $this->assertTrue(Session::check('key1'));
+ $this->assertTrue(Session::check('key2'));
- $result = Session::check('key1', array('name' => 'persistent'));
- $this->assertTrue($result);
+ $this->assertTrue(Session::check('key1', array('name' => 'persistent')));
+ $this->assertFalse(Session::check('key1', array('name' => 'temp')));
- $result = Session::check('key1', array('name' => 'temp'));
- $this->assertFalse($result);
-
- $result = Session::check('key2', array('name' => 'persistent'));
- $this->assertFalse($result);
-
- $result = Session::check('key2', array('name' => 'temp'));
- $this->assertTrue($result);
+ $this->assertFalse(Session::check('key2', array('name' => 'persistent')));
+ $this->assertTrue(Session::check('key2', array('name' => 'temp')));
Session::delete('key1');
- $result = Session::check('key1');
- $this->assertFalse($result);
+ $this->assertFalse(Session::check('key1'));
Session::write('key1', 'value', array('name' => 'persistent'));
- $result = Session::check('key1');
- $this->assertTrue($result);
+ $this->assertTrue(Session::check('key1'));
Session::delete('key1', array('name' => 'temp'));
- $result = Session::check('key1');
- $this->assertTrue($result);
+ $this->assertTrue(Session::check('key1'));
Session::delete('key1', array('name' => 'persistent'));
- $result = Session::check('key1');
- $this->assertFalse($result);
+ $this->assertFalse(Session::check('key1'));
+ }
+
+ /**
+ * Tests clearing all session data from one or all adapters.
+ *
+ * @return void
+ */
+ public function testSessionClear() {
+ Session::config(array(
+ 'primary' => array('adapter' => new Memory(), 'filters' => array()),
+ 'secondary' => array('adapter' => new Memory(), 'filters' => array())
+ ));
+ Session::write('key1', 'value', array('name' => 'primary'));
+ Session::write('key2', 'value', array('name' => 'secondary'));
+
+ Session::clear(array('name' => 'secondary'));
+ $this->assertTrue(Session::check('key1'));
+ $this->assertFalse(Session::check('key2'));
+
+ Session::write('key2', 'value', array('name' => 'secondary'));
+ Session::clear();
+ $this->assertFalse(Session::check('key1'));
+ $this->assertFalse(Session::check('key2'));
}
/**
* Tests querying session keys from the primary adapter.
- * The memory adapter returns a UUID based on a server variable for portability.
+ * The memory adapter returns a UUID.
*
* @return void
*/
@@ -175,7 +189,7 @@ class SessionTest extends \lithium\test\Unit {
public function testSessionState() {
$this->assertTrue(Session::isStarted());
$this->assertTrue(Session::isStarted('default'));
- $this->expectException('Configuration invalid has not been defined');
+ $this->expectException("Configuration `invalid` has not been defined.");
$this->assertFalse(Session::isStarted('invalid'));
}
@@ -186,9 +200,52 @@ class SessionTest extends \lithium\test\Unit {
public function testSessionStateResetNamed() {
Session::reset();
- $this->expectException('Configuration default has not been defined');
+ $this->expectException("Configuration `default` has not been defined.");
$this->assertFalse(Session::isStarted('default'));
}
+
+ public function testReadFilter() {
+ Session::config(array(
+ 'primary' => array('adapter' => new Memory(), 'filters' => array()),
+ 'secondary' => array('adapter' => new Memory(), 'filters' => array())
+ ));
+ Session::applyFilter('read', function($self, $params, $chain) {
+ $result = $chain->next($self, $params, $chain);
+
+ if (isset($params['options']['increment'])) {
+ $result += $params['options']['increment'];
+ }
+ return $result;
+ });
+ Session::write('foo', 'bar');
+ $this->assertEqual('bar', Session::read('foo'));
+
+ Session::write('bar', 1);
+ $this->assertEqual(2, Session::read('bar', array('increment' => 1)));
+ }
+
+ public function testStrategies() {
+ Session::config(array('primary' => array(
+ 'adapter' => new Memory(), 'filters' => array(), 'strategies' => array(
+ 'lithium\storage\cache\strategy\Json'
+ )
+ )));
+
+ Session::write('test', array('foo' => 'bar'));
+ $this->assertEqual(array('foo' => 'bar'), Session::read('test'));
+
+ $this->assertTrue(Session::check('test'));
+ $this->assertTrue(Session::check('test', array('strategies' => false)));
+
+ $result = Session::read('test', array('strategies' => false));
+ $this->assertEqual('{"foo":"bar"}', $result);
+
+ $result = Session::clear(array('strategies' => false));
+ $this->assertNull(Session::read('test'));
+
+ $this->assertFalse(Session::check('test'));
+ $this->assertFalse(Session::check('test', array('strategies' => false)));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/cache/adapter/ApcTest.php b/libraries/lithium/tests/cases/storage/cache/adapter/ApcTest.php
index ca70011..5879178 100644
--- a/libraries/lithium/tests/cases/storage/cache/adapter/ApcTest.php
+++ b/libraries/lithium/tests/cases/storage/cache/adapter/ApcTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\adapter;
-use \lithium\storage\cache\adapter\Apc;
+use lithium\storage\cache\adapter\Apc;
class ApcTest extends \lithium\test\Unit {
@@ -42,7 +42,6 @@ class ApcTest extends \lithium\test\Unit {
$key = 'key';
$data = 'value';
$expiry = '+5 seconds';
- $time = strtotime($expiry);
$closure = $this->Apc->write($key, $data, $expiry);
$this->assertTrue(is_callable($closure));
@@ -61,7 +60,6 @@ class ApcTest extends \lithium\test\Unit {
$key = 'another_key';
$data = 'more_data';
$expiry = '+1 minute';
- $time = strtotime($expiry);
$closure = $this->Apc->write($key, $data, $expiry);
$this->assertTrue(is_callable($closure));
@@ -78,9 +76,28 @@ class ApcTest extends \lithium\test\Unit {
$this->assertTrue($result);
}
+ public function testWriteDefaultCacheTime() {
+ $Apc = new Apc(array('expiry' => '+5 seconds'));
+ $key = 'key';
+ $data = 'value';
+
+ $closure = $Apc->write($key, $data);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data');
+ $result = $closure($Apc, $params, null);
+ $expected = $data;
+ $this->assertTrue($result);
+
+ $result = apc_fetch($key);
+ $this->assertEqual($expected, $result);
+
+ $result = apc_delete($key);
+ $this->assertTrue($result);
+ }
+
public function testWriteMulti() {
$expiry = '+1 minute';
- $time = strtotime($expiry);
$key = array(
'key1' => 'data1',
'key2' => 'data2',
@@ -106,7 +123,6 @@ class ApcTest extends \lithium\test\Unit {
public function testSimpleRead() {
$key = 'read_key';
$data = 'read data';
- $time = strtotime('+1 minute');
$result = apc_store($key, $data, 60);
$this->assertTrue($result);
@@ -124,7 +140,6 @@ class ApcTest extends \lithium\test\Unit {
$key = 'another_read_key';
$data = 'read data';
- $time = strtotime('+1 minute');
$result = apc_store($key, $data, 60);
$this->assertTrue($result);
@@ -144,7 +159,6 @@ class ApcTest extends \lithium\test\Unit {
public function testReadMulti() {
$expiry = '+1 minute';
- $time = strtotime($expiry);
$key = array(
'key1' => 'data1',
'key2' => 'data2',
@@ -184,7 +198,6 @@ class ApcTest extends \lithium\test\Unit {
public function testDelete() {
$key = 'delete_key';
$data = 'data to delete';
- $time = strtotime('+1 minute');
$result = apc_store($key, $data, 60);
$this->assertTrue($result);
@@ -199,7 +212,6 @@ class ApcTest extends \lithium\test\Unit {
public function testDeleteMulti() {
$expiry = '+1 minute';
- $time = strtotime($expiry);
$key = array(
'key1' => 'data1',
'key2' => 'data2',
@@ -226,7 +238,6 @@ class ApcTest extends \lithium\test\Unit {
public function testDeleteNonExistentKey() {
$key = 'delete_key';
$data = 'data to delete';
- $time = strtotime('+1 minute');
$closure = $this->Apc->delete($key);
$this->assertTrue(is_callable($closure));
@@ -240,7 +251,6 @@ class ApcTest extends \lithium\test\Unit {
$key = 'write_read_key';
$data = 'write/read value';
$expiry = '+5 seconds';
- $time = strtotime($expiry);
$closure = $this->Apc->write($key, $data, $expiry);
$this->assertTrue(is_callable($closure));
@@ -272,12 +282,11 @@ class ApcTest extends \lithium\test\Unit {
public function testClear() {
$key1 = 'key_clear_1';
$key2 = 'key_clear_2';
- $time = strtotime('+1 minute');
- $result = apc_store($key1, 'data that will no longer exist', $time);
+ $result = apc_store($key1, 'data that will no longer exist', 60);
$this->assertTrue($result);
- $result = apc_store($key2, 'more dead data', $time);
+ $result = apc_store($key2, 'more dead data', 60);
$this->assertTrue($result);
$result = $this->Apc->clear();
@@ -288,11 +297,10 @@ class ApcTest extends \lithium\test\Unit {
}
public function testDecrement() {
- $time = strtotime('+1 minute');
$key = 'decrement';
$value = 10;
- $result = apc_store($key, $value, $time);
+ $result = apc_store($key, $value, 60);
$this->assertTrue($result);
$closure = $this->Apc->decrement($key);
@@ -310,11 +318,10 @@ class ApcTest extends \lithium\test\Unit {
}
public function testDecrementNonIntegerValue() {
- $time = strtotime('+1 minute');
$key = 'non_integer';
$value = 'no';
- $result = apc_store($key, $value, $time);
+ $result = apc_store($key, $value, 60);
$this->assertTrue($result);
$closure = $this->Apc->decrement($key);
@@ -331,11 +338,10 @@ class ApcTest extends \lithium\test\Unit {
}
public function testIncrement() {
- $time = strtotime('+1 minute');
$key = 'increment';
$value = 10;
- $result = apc_store($key, $value, $time);
+ $result = apc_store($key, $value, 60);
$this->assertTrue($result);
$closure = $this->Apc->increment($key);
@@ -353,11 +359,10 @@ class ApcTest extends \lithium\test\Unit {
}
public function testIncrementNonIntegerValue() {
- $time = strtotime('+1 minute');
$key = 'non_integer_increment';
$value = 'yes';
- $result = apc_store($key, $value, $time);
+ $result = apc_store($key, $value, 60);
$this->assertTrue($result);
$closure = $this->Apc->increment($key);
diff --git a/libraries/lithium/tests/cases/storage/cache/adapter/FileTest.php b/libraries/lithium/tests/cases/storage/cache/adapter/FileTest.php
index 7c3a88c..01cc90f 100644
--- a/libraries/lithium/tests/cases/storage/cache/adapter/FileTest.php
+++ b/libraries/lithium/tests/cases/storage/cache/adapter/FileTest.php
@@ -2,35 +2,49 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\adapter;
-use \lithium\storage\cache\adapter\File;
-use \SplFileInfo;
+use SplFileInfo;
+use lithium\core\Libraries;
+use lithium\storage\cache\adapter\File;
class FileTest extends \lithium\test\Unit {
/**
+ * Checks whether the 'empty' file exists in `app/resources/tmp/cache` and, if so, ensures
+ * that it is restored at the end of the testing cycle.
+ *
+ * @var string
+ */
+ protected $_hasEmpty = true;
+
+ /**
* Skip the test if the default File adapter read/write path
* is not read/write-able.
*
* @return void
*/
public function skip() {
- $directory = new SplFileInfo(LITHIUM_APP_PATH . "/resources/tmp/cache/");
+ $directory = new SplFileInfo(Libraries::get(true, 'resources') . "/tmp/cache/");
$accessible = ($directory->isDir() && $directory->isReadable() && $directory->isWritable());
$message = 'The File cache adapter path does not have the proper permissions.';
$this->skipIf(!$accessible, $message);
}
public function setUp() {
+ $this->_hasEmpty = file_exists(Libraries::get(true, 'resources') . "/tmp/cache/empty");
$this->File = new File();
}
public function tearDown() {
+ if ($this->_hasEmpty) {
+ touch(Libraries::get(true, 'resources') . "/tmp/cache/empty");
+ touch(Libraries::get(true, 'resources') . "/tmp/cache/templates/empty");
+ }
unset($this->File);
}
@@ -53,14 +67,38 @@ class FileTest extends \lithium\test\Unit {
$expected = 25;
$this->assertEqual($expected, $result);
- $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
+ $this->assertTrue(file_exists(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"));
$this->assertEqual(
- file_get_contents(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"),
+ file_get_contents(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"),
"{:expiry:$time}\ndata"
);
- $this->assertTrue(unlink(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
- $this->assertFalse(file_exists(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
+ $this->assertTrue(unlink(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"));
+ $this->assertFalse(file_exists(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"));
+ }
+
+ public function testWriteDefaultCacheExpiry() {
+ $File = new File(array('expiry' => '+1 minute'));
+ $key = 'default_keykey';
+ $data = 'data';
+ $time = time() + 60;
+
+ $closure = $File->write($key, $data);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data');
+ $result = $closure($File, $params, null);
+ $expected = 25;
+ $this->assertEqual($expected, $result);
+
+ $this->assertTrue(file_exists(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"));
+ $this->assertEqual(
+ file_get_contents(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"),
+ "{:expiry:{$time}}\ndata"
+ );
+
+ $this->assertTrue(unlink(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"));
+ $this->assertFalse(file_exists(Libraries::get(true, 'resources') . "/tmp/cache/{$key}"));
}
public function testRead() {
@@ -70,16 +108,15 @@ class FileTest extends \lithium\test\Unit {
$closure = $this->File->read($key);
$this->assertTrue(is_callable($closure));
- file_put_contents(LITHIUM_APP_PATH . "/resources/tmp/cache/$key", "{:expiry:$time}\ndata");
- $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
+ $path = Libraries::get(true, 'resources') . "/tmp/cache/{$key}";
+ file_put_contents($path, "{:expiry:$time}\ndata");
+ $this->assertTrue(file_exists($path));
$params = compact('key');
$result = $closure($this->File, $params, null);
- $expected = 'data';
- $this->assertEqual($expected, $result);
+ $this->assertEqual('data', $result);
- $this->assertTrue(unlink(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
- $this->assertFalse(file_exists(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
+ unlink($path);
$key = 'non_existent';
$params = compact('key');
@@ -96,47 +133,47 @@ class FileTest extends \lithium\test\Unit {
$closure = $this->File->read($key);
$this->assertTrue(is_callable($closure));
+ $path = Libraries::get(true, 'resources') . "/tmp/cache/{$key}";
- file_put_contents(LITHIUM_APP_PATH . "/resources/tmp/cache/$key", "{:expiry:$time}\ndata");
- $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
+ file_put_contents($path, "{:expiry:$time}\ndata");
+ $this->assertTrue(file_exists($path));
sleep(2);
$params = compact('key');
- $result = $closure($this->File, $params, null);
- $this->assertFalse($result);
+ $this->assertFalse($closure($this->File, $params, null));
}
public function testDelete() {
$key = 'key_to_delete';
$time = time() + 1;
+ $path = Libraries::get(true, 'resources') . "/tmp/cache/{$key}";
- file_put_contents(LITHIUM_APP_PATH . "/resources/tmp/cache/$key", "{:expiry:$time}\ndata");
- $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
+ file_put_contents($path, "{:expiry:$time}\ndata");
+ $this->assertTrue(file_exists($path));
$closure = $this->File->delete($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->File, $params, null);
- $this->assertTrue($result);
+ $this->assertTrue($closure($this->File, $params, null));
$key = 'non_existent';
$params = compact('key');
- $result = $closure($this->File, $params, null);
- $this->assertFalse($result);
+ $this->assertFalse($closure($this->File, $params, null));
}
public function testClear() {
$key = 'key_to_clear';
$time = time() + 1;
- file_put_contents(LITHIUM_APP_PATH . "/resources/tmp/cache/$key", "{:expiry:$time}\ndata");
+ $path = Libraries::get(true, 'resources') . "/tmp/cache/{$key}";
+ file_put_contents($path, "{:expiry:$time}\ndata");
$result = $this->File->clear();
$this->assertTrue($result);
- $this->assertFalse(file_exists(LITHIUM_APP_PATH . "/resources/tmp/cache/$key"));
+ $this->assertFalse(file_exists($path));
- $result = touch(LITHIUM_APP_PATH . "/resources/tmp/cache/empty");
+ $result = touch(Libraries::get(true, 'resources') . "/tmp/cache/empty");
$this->assertTrue($result);
}
@@ -151,8 +188,6 @@ class FileTest extends \lithium\test\Unit {
$result = $this->File->decrement($key);
$this->assertEqual(false, $result);
}
-
-
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/cache/adapter/MemcacheTest.php b/libraries/lithium/tests/cases/storage/cache/adapter/MemcacheTest.php
index 0dc7d38..7930a7a 100644
--- a/libraries/lithium/tests/cases/storage/cache/adapter/MemcacheTest.php
+++ b/libraries/lithium/tests/cases/storage/cache/adapter/MemcacheTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\adapter;
-use \lithium\storage\cache\adapter\Memcache;
+use lithium\storage\cache\adapter\Memcache;
class MemcacheTest extends \lithium\test\Unit {
@@ -88,6 +88,27 @@ class MemcacheTest extends \lithium\test\Unit {
$this->assertTrue($result);
}
+ public function testWriteDefaultCacheExpiry() {
+ $Memcache = new Memcache(array('expiry' => '+5 seconds'));
+ $key = 'default_key';
+ $data = 'value';
+
+ $closure = $Memcache->write($key, $data);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data');
+ $result = $closure($Memcache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->get($key);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->delete($key);
+ $this->assertTrue($result);
+
+ }
+
public function testWriteMulti() {
$expiry = '+1 minute';
$time = strtotime($expiry);
diff --git a/libraries/lithium/tests/cases/storage/cache/adapter/MemoryTest.php b/libraries/lithium/tests/cases/storage/cache/adapter/MemoryTest.php
index 790fcd0..b42c33d 100644
--- a/libraries/lithium/tests/cases/storage/cache/adapter/MemoryTest.php
+++ b/libraries/lithium/tests/cases/storage/cache/adapter/MemoryTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\adapter;
-use \lithium\storage\cache\adapter\Memory;
+use lithium\storage\cache\adapter\Memory;
class MemoryTest extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/storage/cache/adapter/RedisTest.php b/libraries/lithium/tests/cases/storage/cache/adapter/RedisTest.php
index 7db90e5..40170e9 100644
--- a/libraries/lithium/tests/cases/storage/cache/adapter/RedisTest.php
+++ b/libraries/lithium/tests/cases/storage/cache/adapter/RedisTest.php
@@ -2,71 +2,88 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\adapter;
-use \lithium\storage\cache\adapter\Redis;
+use Exception;
+use Redis as RedisCore;
+use lithium\storage\cache\adapter\Redis;
class RedisTest extends \lithium\test\Unit {
+ public function __construct(array $config = array()) {
+ $defaults = array(
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ );
+ parent::__construct($config + $defaults);
+ }
+
/**
* Skip the test if the Redis extension is unavailable.
*
* @return void
*/
public function skip() {
- $extensionExists = extension_loaded('redis');
- $message = 'The redis extension is not installed.';
- $this->skipIf(!$extensionExists, $message);
-
- $R = new \Redis();
- $R->connect('127.0.0.1', 6379);
- $message = 'redis-server does not appear to be running on 127.0.0.1:6379';
- $result = $R->info();
- $this->skipIf(empty($result), $message);
- unset($R);
+ $this->skipIf(!Redis::enabled(), 'The redis extension is not installed.');
+
+ $redis = new RedisCore();
+ $cfg = $this->_config;
+
+ try {
+ $redis->connect($cfg['host'], $cfg['port']);
+ } catch (Exception $e) {
+ $info = $redis->info();
+ $msg = "redis-server does not appear to be running on {$cfg['host']}:{$cfg['port']}";
+ $this->skipIf(!$info, $msg);
+ }
+ unset($redis);
}
public function setUp() {
- $this->server = array('host' => '127.0.0.1', 'port' => 6379);
- $this->_Redis = new \Redis();
- $this->_Redis->connect($this->server['host'], $this->server['port']);
- $this->Redis = new Redis();
+ $this->_redis = new RedisCore();
+ $this->_redis->connect($this->_config['host'], $this->_config['port']);
+ $this->redis = new Redis();
}
public function tearDown() {
- $this->_Redis->flushdb();
+ $this->_redis->flushdb();
}
public function testEnabled() {
- $redis = $this->Redis;
+ $redis = $this->redis;
$this->assertTrue($redis::enabled());
}
+ public function testInit() {
+ $redis = new Redis();
+ $this->assertTrue($redis->connection instanceof RedisCore);
+ }
+
public function testSimpleWrite() {
$key = 'key';
$data = 'value';
$expiry = '+5 seconds';
$time = strtotime($expiry);
- $closure = $this->Redis->write($key, $data, $expiry);
+ $closure = $this->redis->write($key, $data, $expiry);
$this->assertTrue(is_callable($closure));
$params = compact('key', 'data', 'expiry');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$expected = $data;
$this->assertEqual($expected, $result);
- $result = $this->_Redis->get($key);
+ $result = $this->_redis->get($key);
$this->assertEqual($expected, $result);
- $result = $this->_Redis->ttl($key);
+ $result = $this->_redis->ttl($key);
$this->assertEqual($time - time(), $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
$key = 'another_key';
@@ -74,72 +91,141 @@ class RedisTest extends \lithium\test\Unit {
$expiry = '+1 minute';
$time = strtotime($expiry);
- $closure = $this->Redis->write($key, $data, $expiry);
+ $closure = $this->redis->write($key, $data, $expiry);
$this->assertTrue(is_callable($closure));
$params = compact('key', 'data', 'expiry');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$expected = $data;
$this->assertEqual($expected, $result);
- $result = $this->_Redis->get($key);
+ $result = $this->_redis->get($key);
$this->assertEqual($expected, $result);
- $result = $this->_Redis->ttl($key);
+ $result = $this->_redis->ttl($key);
$this->assertEqual($time - time(), $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
}
+ public function testWriteDefaultCacheExpiry() {
+ $redis = new Redis(array('expiry' => '+5 seconds'));
+ $key = 'default_key';
+ $data = 'value';
+ $time = strtotime('+5 seconds');
+
+ $closure = $redis->write($key, $data);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data');
+ $result = $closure($redis, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_redis->get($key);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_redis->ttl($key);
+ $this->assertEqual($time - time(), $result);
+
+ $result = $this->_redis->delete($key);
+ $this->assertTrue($result);
+ }
+
+ public function testWriteNoCacheExpiry() {
+ $redis = new Redis(array('expiry' => null));
+ $key = 'default_key';
+ $data = 'value';
+ $redis->write($key, $data)->__invoke(null, compact('key', 'data'), null);
+ $this->assertEqual($data, $this->_redis->get($key));
+ $this->assertTrue($this->_redis->delete($key));
+ }
+
public function testSimpleRead() {
$key = 'read_key';
$data = 'read data';
- $time = strtotime('+1 minute');
- $result = $this->_Redis->set($key, $data);
+ $result = $this->_redis->set($key, $data);
$this->assertTrue($result);
- $closure = $this->Redis->read($key);
+ $closure = $this->redis->read($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$expected = $data;
$this->assertEqual($expected, $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
$key = 'another_read_key';
$data = 'read data';
$time = strtotime('+1 minute');
- $result = $this->_Redis->set($key, $data);
+ $result = $this->_redis->set($key, $data);
$this->assertTrue($result);
- $result = $this->_Redis->ttl($key);
+ $result = $this->_redis->ttl($key);
$this->assertTrue($result);
- $closure = $this->Redis->read($key);
+ $closure = $this->redis->read($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$expected = $data;
$this->assertEqual($expected, $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
}
+ public function testMultiRead() {
+ $data = array('key1' => 'value1', 'key2' => 'value2');
+ $result = $this->_redis->mset($data);
+ $this->assertTrue($result);
+
+ $closure = $this->redis->read(array_keys($data));
+ $this->assertTrue(is_callable($closure));
+
+ $params = array('key' => array_keys($data));
+ $result = $closure($this->redis, $params, null);
+ $expected = array_values($data);
+ $this->assertEqual($expected, $result);
+
+ foreach ($data as $k => $v) {
+ $result = $this->_redis->delete($k);
+ $this->assertTrue($result);
+ }
+ }
+
+ public function testMultiWrite() {
+ $key = array('key1' => 'value1', 'key2' => 'value2');
+ $expiry = '+5 seconds';
+ $time = strtotime($expiry);
+
+ $closure = $this->redis->write($key, $expiry);
+ $this->assertTrue(is_callable($closure));
+
+ $params = array('key' => $key, 'data' => $expiry, 'expiry' => null);
+ $result = $closure($this->redis, $params, null);
+ $expected = array('key1' => true, 'key2' => true);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_redis->getMultiple(array_keys($key));
+ $expected = array_values($key);
+ $this->assertEqual($expected, $result);
+ }
+
public function testReadKeyThatDoesNotExist() {
$key = 'does_not_exist';
- $closure = $this->Redis->read($key);
+ $closure = $this->redis->read($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$this->assertFalse($result);
}
@@ -149,26 +235,26 @@ class RedisTest extends \lithium\test\Unit {
$data = 'data to delete';
$time = strtotime('+1 minute');
- $result = $this->_Redis->set($key, $data);
+ $result = $this->_redis->set($key, $data);
$this->assertTrue($result);
- $closure = $this->Redis->delete($key);
+ $closure = $this->redis->delete($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$this->assertTrue($result);
- $this->assertFalse($this->_Redis->delete($key));
+ $this->assertFalse($this->_redis->delete($key));
}
public function testDeleteNonExistentKey() {
$key = 'delete_key';
- $closure = $this->Redis->delete($key);
+ $closure = $this->redis->delete($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$this->assertFalse($result);
}
@@ -178,67 +264,67 @@ class RedisTest extends \lithium\test\Unit {
$expiry = '+5 seconds';
$time = strtotime($expiry);
- $closure = $this->Redis->write($key, $data, $expiry);
+ $closure = $this->redis->write($key, $data, $expiry);
$this->assertTrue(is_callable($closure));
$params = compact('key', 'data', 'expiry');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$expected = $data;
$this->assertEqual($expected, $result);
- $result = $this->_Redis->get($key);
+ $result = $this->_redis->get($key);
$this->assertEqual($expected, $result);
- $closure = $this->Redis->read($key);
+ $closure = $this->redis->read($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$expected = $data;
$this->assertEqual($expected, $result);
- $closure = $this->Redis->delete($key);
+ $closure = $this->redis->delete($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$this->assertTrue($result);
- $this->assertFalse($this->_Redis->get($key));
+ $this->assertFalse($this->_redis->get($key));
}
public function testClear() {
- $result = $this->_Redis->set('key', 'value');
+ $result = $this->_redis->set('key', 'value');
$this->assertTrue($result);
- $result = $this->_Redis->set('another_key', 'value');
+ $result = $this->_redis->set('another_key', 'value');
$this->assertTrue($result);
- $result = $this->Redis->clear();
+ $result = $this->redis->clear();
$this->assertTrue($result);
- $this->assertFalse($this->_Redis->get('key'));
- $this->assertFalse($this->_Redis->get('another_key'));
+ $this->assertFalse($this->_redis->get('key'));
+ $this->assertFalse($this->_redis->get('another_key'));
}
public function testDecrement() {
$key = 'decrement';
$value = 10;
- $result = $this->_Redis->set($key, $value);
+ $result = $this->_redis->set($key, $value);
$this->assertTrue($result);
- $closure = $this->Redis->decrement($key);
+ $closure = $this->redis->decrement($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$this->assertEqual($value - 1, $result);
- $result = $this->_Redis->get($key);
+ $result = $this->_redis->get($key);
$this->assertEqual($value - 1, $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
}
@@ -246,19 +332,20 @@ class RedisTest extends \lithium\test\Unit {
$key = 'non_integer';
$value = 'no';
- $result = $this->_Redis->set($key, $value);
+ $result = $this->_redis->set($key, $value);
$this->assertTrue($result);
- $closure = $this->Redis->decrement($key);
+ $closure = $this->redis->decrement($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
+ $this->assertFalse($result);
- $result = $this->_Redis->get($key);
- $this->assertEqual(-1, $result);
+ $result = $this->_redis->get($key);
+ $this->assertEqual($value, $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
}
@@ -266,20 +353,20 @@ class RedisTest extends \lithium\test\Unit {
$key = 'increment';
$value = 10;
- $result = $this->_Redis->set($key, $value);
+ $result = $this->_redis->set($key, $value);
$this->assertTrue($result);
- $closure = $this->Redis->increment($key);
+ $closure = $this->redis->increment($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
$this->assertEqual($value + 1, $result);
- $result = $this->_Redis->get($key);
+ $result = $this->_redis->get($key);
$this->assertEqual($value + 1, $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
}
@@ -287,19 +374,20 @@ class RedisTest extends \lithium\test\Unit {
$key = 'non_integer_increment';
$value = 'yes';
- $result = $this->_Redis->set($key, $value);
+ $result = $this->_redis->set($key, $value);
$this->assertTrue($result);
- $closure = $this->Redis->increment($key);
+ $closure = $this->redis->increment($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Redis, $params, null);
+ $result = $closure($this->redis, $params, null);
+ $this->assertFalse($result);
- $result = $this->_Redis->get($key);
- $this->assertEqual(1, $result);
+ $result = $this->_redis->get($key);
+ $this->assertEqual($value, $result);
- $result = $this->_Redis->delete($key);
+ $result = $this->_redis->delete($key);
$this->assertTrue($result);
}
}
diff --git a/libraries/lithium/tests/cases/storage/cache/adapter/XCacheTest.php b/libraries/lithium/tests/cases/storage/cache/adapter/XCacheTest.php
index 57e5e4f..61bc4bf 100644
--- a/libraries/lithium/tests/cases/storage/cache/adapter/XCacheTest.php
+++ b/libraries/lithium/tests/cases/storage/cache/adapter/XCacheTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\adapter;
-use \lithium\storage\cache\adapter\XCache;
+use lithium\storage\cache\adapter\XCache;
class XCacheTest extends \lithium\test\Unit {
@@ -86,6 +86,28 @@ class XCacheTest extends \lithium\test\Unit {
$this->assertTrue($result);
}
+ public function testWriteDefaultCacheExpiry() {
+ $XCache = new XCache(array('expiry' => '+5 seconds'));
+ $key = 'default_key';
+ $data = 'value';
+ $time = strtotime('+5 seconds');
+
+ $closure = $XCache->write($key, $data);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data');
+ $result = $closure($XCache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = xcache_get($key);
+ $this->assertEqual($expected, $result);
+
+ $result = xcache_unset($key);
+ $this->assertTrue($result);
+
+ }
+
public function testSimpleRead() {
$key = 'read_key';
$data = 'read data';
diff --git a/libraries/lithium/tests/cases/storage/cache/strategy/Base64Test.php b/libraries/lithium/tests/cases/storage/cache/strategy/Base64Test.php
index cffda16..7d96d32 100644
--- a/libraries/lithium/tests/cases/storage/cache/strategy/Base64Test.php
+++ b/libraries/lithium/tests/cases/storage/cache/strategy/Base64Test.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\strategy;
-use \lithium\storage\cache\strategy\Base64;
+use lithium\storage\cache\strategy\Base64;
class Base64Test extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/storage/cache/strategy/JsonTest.php b/libraries/lithium/tests/cases/storage/cache/strategy/JsonTest.php
index 5e8be16..b3c1791 100644
--- a/libraries/lithium/tests/cases/storage/cache/strategy/JsonTest.php
+++ b/libraries/lithium/tests/cases/storage/cache/strategy/JsonTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\cache\strategy;
-use \lithium\storage\cache\strategy\Json;
+use lithium\storage\cache\strategy\Json;
class JsonTest extends \lithium\test\Unit {
@@ -27,7 +27,7 @@ class JsonTest extends \lithium\test\Unit {
$expected = array('some' => 'data');
$encoded = json_encode($expected);
$result = $this->Json->read($encoded);
- $this->assertEqual((object) $expected, $result);
+ $this->assertEqual($expected, $result);
}
}
diff --git a/libraries/lithium/tests/cases/storage/session/adapter/CookieTest.php b/libraries/lithium/tests/cases/storage/session/adapter/CookieTest.php
index 7115e62..98cee75 100644
--- a/libraries/lithium/tests/cases/storage/session/adapter/CookieTest.php
+++ b/libraries/lithium/tests/cases/storage/session/adapter/CookieTest.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of Rad, Inc. (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of Rad, Inc. (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\session\adapter;
-use \lithium\storage\session\adapter\Cookie;
+use lithium\util\Inflector;
+use lithium\storage\session\adapter\Cookie;
class CookieTest extends \lithium\test\Unit {
@@ -23,48 +24,46 @@ class CookieTest extends \lithium\test\Unit {
$this->skipIf($sapi === 'cli', $message);
}
- public function assertCookie($expected, $headers) {
- $key = $expected['key'];
- $value = preg_quote(urlencode($expected['value']), '/');
+ public function setUp() {
+ $this->cookie = new Cookie();
+ $path = explode('/', LITHIUM_APP_PATH);
+ $this->name = end($path) . 'cookie';
+ }
- if (isset($expected['expires'])) {
- $date = gmdate('D, d-M-Y H:i:s \G\M\T', strtotime($expected['expires']));
- $expires = preg_quote($date, '/');
- } else {
- $expires = ".+?";
- }
- $path = preg_quote($expected['path'], '/');
- $pattern = "/^Set\-Cookie:\sapp\[$key\]=$value;\sexpires=$expires;\spath=$path/";
- $match = false;
-
- foreach ($headers as $header) {
- if (preg_match($pattern, $header)) {
- $match = true;
- continue;
- }
- }
+ public function tearDown() {
+ $this->_destroyCookie($this->name);
+ $cookies = array_keys($_COOKIE);
- if (!$match) {
- $this->assert(false, sprintf('{:message} - Cookie %s not found in headers.', $pattern));
- return false;
+ foreach ($cookies as $cookie) {
+ setcookie($cookie, "", time()-1);
}
- return $this->assert(true, '%s');
}
- public function setUp() {
- $this->Cookie = new Cookie();
+ protected function _destroyCookie($name = null) {
+ if (!$name) {
+ $name = session_name();
+ }
+ $settings = session_get_cookie_params();
+ setcookie(
+ $name, '', time() - 1000, $settings['path'], $settings['domain'],
+ $settings['secure'], $settings['httponly']
+ );
+ if (session_id()) {
+ session_destroy();
+ }
+ $_COOKIE = array();
}
public function testEnabled() {
- $this->assertTrue($this->Cookie->isEnabled());
+ $this->assertTrue($this->cookie->isEnabled());
}
public function testKey() {
- $this->assertEqual('app', $this->Cookie->key());
+ $this->assertEqual($this->name, $this->cookie->key());
}
public function testIsStarted() {
- $this->assertTrue($this->Cookie->isStarted());
+ $this->assertTrue($this->cookie->isStarted());
}
public function testWriteDefaultParameters() {
@@ -73,13 +72,68 @@ class CookieTest extends \lithium\test\Unit {
$expires = "+2 days";
$path = '/';
- $closure = $this->Cookie->write($key, $value);
+ $closure = $this->cookie->write($key, $value);
$this->assertTrue(is_callable($closure));
$params = compact('key', 'value');
- $result = $closure($this->Cookie, $params, null);
+ $result = $closure($this->cookie, $params, null);
+
+ $this->assertCookie(compact('key', 'value', 'expires', 'path'));
+ }
+
+ public function testCustomCookieName() {
+ $Cookie = new Cookie(array('name' => 'test'));
+ $this->assertEqual('test', $Cookie->key());
+ }
+
+
+ public function testWriteArrayData() {
+ $key = 'user';
+ $value = array(
+ 'email' => 'test@localhost',
+ 'name' => 'Testy McTesterson',
+ 'address' => array('country' => 'Iran', 'city' => 'Mashhad')
+ );
+ $expires = "+2 days";
+ $path = '/';
+
+ $closure = $this->cookie->write($key, $value);
+ $this->assertTrue(is_callable($closure));
+ $params = compact('key', 'value');
+ $result = $closure($this->cookie, $params, null);
+
+ $expected = compact('expires');
+ $expected += array('key' => 'user.email', 'value' => 'test@localhost');
+ $this->assertCookie($expected);
+
+ $expected = compact('expires');
+ $expected += array('key' => 'user.address.country', 'value' => 'Iran');
+ $this->assertCookie($expected);
+ }
+
+ public function testReadDotSyntax() {
+ $key = 'read.test';
+ $value = 'value to be read';
+ $_COOKIE[$this->name]['read']['test'] = $value;
+
+ $closure = $this->cookie->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->cookie, $params, null);
+ $this->assertEqual($value, $result);
+
+ $result = $closure($this->cookie, array('key' => null), null);
+ $this->assertEqual($_COOKIE[$this->name], $result);
+
+ $key = 'does_not_exist';
+ $closure = $this->cookie->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->cookie, $params, null);
+ $this->assertNull($result);
- $this->assertCookie(compact('key', 'value', 'expires', 'path'), headers_list());
}
public function testWriteCustomParameters() {
@@ -89,78 +143,144 @@ class CookieTest extends \lithium\test\Unit {
$path = '/';
$options = array('expire' => $expires);
- $closure = $this->Cookie->write($key, $value, $options);
+ $closure = $this->cookie->write($key, $value, $options);
$this->assertTrue(is_callable($closure));
$params = compact('key', 'value', 'options');
- $result = $closure($this->Cookie, $params, null);
+ $result = $closure($this->cookie, $params, null);
- $this->assertCookie(compact('key', 'value', 'expires', 'path'), headers_list());
+ $this->assertCookie(compact('key', 'value', 'expires', 'path'));
}
public function testRead() {
$key = 'read';
$value = 'value to be read';
- $_COOKIE[$key] = $value;
+ $_COOKIE[$this->name][$key] = $value;
- $closure = $this->Cookie->read($key);
+ $closure = $this->cookie->read($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Cookie, $params, null);
+ $result = $closure($this->cookie, $params, null);
$this->assertEqual($value, $result);
- $result = $closure($this->Cookie, array('key' => null), null);
- $this->assertEqual($_COOKIE, $result);
+ $result = $closure($this->cookie, array('key' => null), null);
+ $this->assertEqual($_COOKIE[$this->name], $result);
$key = 'does_not_exist';
- $closure = $this->Cookie->read($key);
+ $closure = $this->cookie->read($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Cookie, $params, null);
+ $result = $closure($this->cookie, $params, null);
+ $this->assertNull($result);
+ }
+
+ public function testReadNestedKey() {
+ $key = 'User.id';
+ $closure = $this->cookie->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->cookie, $params, null);
$this->assertNull($result);
}
public function testCheck() {
$key = 'read';
$value = 'value to be read';
- $_COOKIE[$key] = $value;
+ $_COOKIE[$this->name][$key] = $value;
- $closure = $this->Cookie->check($key);
+ $closure = $this->cookie->check($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Cookie, $params, null);
+ $result = $closure($this->cookie, $params, null);
$this->assertTrue($result);
$key = 'does_not_exist';
- $closure = $this->Cookie->check($key);
+ $closure = $this->cookie->check($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Cookie, $params, null);
+ $result = $closure($this->cookie, $params, null);
$this->assertFalse($result);
}
- public function testReadArrayOfValues() {
+ public function testClearCookie() {
+ $key = 'clear_key';
+ $value = 'clear_value';
+ $_COOKIE[$this->name][$key] = $value;
+
+ $closure = $this->cookie->check($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->cookie, $params, null);
+ $this->assertTrue($result);
+
+ $closure = $this->cookie->clear();
+ $this->assertTrue(is_callable($closure));
+
+ $params = array();
+ $result = $closure($this->cookie, $params, null);
+ $this->assertTrue($result);
+ $this->assertNoCookie(compact('key', 'value'));
}
+ public function testDeleteArrayData() {
+ $key = 'user';
+ $value = array('email' => 'user@localhost', 'name' => 'Ali');
+ $_COOKIE[$this->name][$key] = $value;
+
+ $closure = $this->cookie->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->cookie, $params, null);
+ $this->assertTrue($result);
+
+ $expected = array('key' => 'user.name', 'value' => 'deleted');
+ $this->assertCookie($expected);
+
+ $expected = array('key' => 'user.email', 'value' => 'deleted');
+ $this->assertCookie($expected);
+ }
+
public function testDeleteNonExistentValue() {
$key = 'delete';
$value = 'deleted';
$path = '/';
- $closure = $this->Cookie->delete($key);
+ $closure = $this->cookie->delete($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
- $result = $closure($this->Cookie, $params, null);
- $this->assertNull($result);
+ $result = $closure($this->cookie, $params, null);
+ $this->assertTrue($result);
+ $this->assertCookie(compact('key', 'value', 'path'));
+ }
+
+ public function testDefaultCookieName() {
+ $cookie = new Cookie();
+ $expected = Inflector::slug(basename(LITHIUM_APP_PATH)) . 'cookie';
+ $this->assertEqual($expected, $cookie->key());
+ }
+
+ public function testBadWrite() {
+ $cookie = new Cookie(array('expire' => null));
+ $this->assertNull($cookie->write('bad', 'val'));
+ }
- $this->assertCookie(compact('key', 'value', 'path'), headers_list());
+ public function testNameWithDotCookie() {
+ $cookie = new Cookie(array('name' => 'my.name'));
+ $key = 'key';
+ $value = 'value';
+ $result = $cookie->write($key, $value)->__invoke($cookie, compact('key', 'value'), null);
+ $this->assertCookie(compact('key', 'value'));
}
+
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/session/adapter/PhpTest.php b/libraries/lithium/tests/cases/storage/session/adapter/PhpTest.php
index c3e90cb..12c2ffd 100644
--- a/libraries/lithium/tests/cases/storage/session/adapter/PhpTest.php
+++ b/libraries/lithium/tests/cases/storage/session/adapter/PhpTest.php
@@ -2,21 +2,24 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of Rad, Inc. (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of Rad, Inc. (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\storage\session\adapter;
-use \lithium\storage\session\adapter\Php;
+use lithium\core\Libraries;
+use lithium\storage\session\adapter\Php;
+use lithium\tests\mocks\storage\session\adapter\MockPhp;
class PhpTest extends \lithium\test\Unit {
public function setUp() {
- if (session_id()) {
- session_destroy();
- }
+ $this->_session = isset($_SESSION) ? $_SESSION : array();
+ $this->_destroySession();
+
$this->Php = new Php();
+ $this->_destroySession();
/* Garbage collection */
$this->_gc_divisor = ini_get('session.gc_divisor');
@@ -24,85 +27,110 @@ class PhpTest extends \lithium\test\Unit {
}
public function tearDown() {
- if (session_id()) {
- session_destroy();
- }
+ $this->_destroySession();
+
/* Revert to original garbage collection probability */
ini_set('session.gc_divisor', $this->_gc_divisor);
+ $_SESSION = $this->_session;
+ }
+ protected function _destroySession($name = null) {
+ if (!$name) {
+ $name = session_name();
+ }
+ $settings = session_get_cookie_params();
+ setcookie(
+ $name, '', time() - 1000, $settings['path'], $settings['domain'],
+ $settings['secure'], $settings['httponly']
+ );
+ if (session_id()) {
+ session_destroy();
+ }
+ $_SESSION = array();
}
public function testEnabled() {
$php = $this->Php;
- $this->assertTrue($php::enabled());
+ $this->_destroySession(session_name());
+ $this->assertFalse($php::enabled());
}
public function testInit() {
$id = session_id();
- $this->assertTrue(!empty($id));
- $this->assertEqual(session_cache_limiter(), "nocache");
-
- $result = $_SESSION['_timestamp'];
- $expected = time();
- $this->assertEqual($expected, $result);
- }
+ $this->assertTrue(empty($id));
- public function testDefaultConfiguration() {
$result = ini_get('session.name');
- $this->assertEqual('', $result);
+ $path = explode('/', LITHIUM_APP_PATH);
+ $this->assertEqual(end($path), $result);
$result = ini_get('session.cookie_lifetime');
- $this->assertEqual(strtotime('+1 day') - time(), (integer) $result);
-
- $result = ini_get('session.cookie_domain');
- $this->assertEqual('', $result);
-
- $result = ini_get('session.cookie_secure');
- $this->assertFalse($result);
-
- $result = ini_get('session.cookie_httponly');
- $this->assertFalse($result);
+ $this->assertEqual(0, (integer) $result);
}
public function testCustomConfiguration() {
$config = array(
- 'name' => 'awesome_name', 'cookie_lifetime' => 1200,
- 'cookie_domain' => 'awesome.domain',
+ 'session.name' => 'awesome_name', 'session.cookie_lifetime' => 1200,
+ 'session.cookie_domain' => 'awesome.domain',
+ 'session.save_path' => Libraries::get(true, 'resources') . '/tmp/',
+ 'somebad.configuration' => 'whoops'
);
$adapter = new Php($config);
$result = ini_get('session.name');
- $this->assertEqual($config['name'], $result);
+ $this->assertEqual($config['session.name'], $result);
$result = ini_get('session.cookie_lifetime');
- $this->assertEqual($config['cookie_lifetime'], (integer) $result);
+ $this->assertEqual($config['session.cookie_lifetime'], (integer) $result);
$result = ini_get('session.cookie_domain');
- $this->assertEqual($config['cookie_domain'], $result);
+ $this->assertEqual($config['session.cookie_domain'], $result);
$result = ini_get('session.cookie_secure');
$this->assertFalse($result);
$result = ini_get('session.cookie_httponly');
$this->assertFalse($result);
+
+ $result = ini_get('session.save_path');
+ $this->assertEqual($config['session.save_path'], $result);
+
+ $result = ini_get('somebad.configuration');
+ $this->assertFalse($result);
}
public function testIsStarted() {
$result = $this->Php->isStarted();
- $this->assertTrue($result);
+ $this->assertFalse($result);
- unset($_SESSION);
+ $this->Php->read();
$result = $this->Php->isStarted();
+ $this->assertTrue($result);
+
+ $this->_destroySession(session_name());
+ $result = $this->Php->isStarted();
+ $this->assertFalse($result);
+ }
+
+ public function testIsStartedNoInit() {
+ $this->_destroySession(session_name());
+
+ $Php = new Php(array('init' => false));
+ $result = $Php->isStarted();
$this->assertFalse($result);
+
+ $Php = new Php();
+ $Php->read();
+ $result = $Php->isStarted();
+ $this->assertTrue($result);
}
public function testKey() {
$result = $this->Php->key();
$this->assertEqual(session_id(), $result);
- session_destroy();
+ $this->_destroySession(session_name());
$result = $this->Php->key();
$this->assertNull($result);
}
@@ -121,6 +149,8 @@ class PhpTest extends \lithium\test\Unit {
}
public function testRead() {
+ $this->Php->read();
+
$key = 'read_test';
$value = 'value to be read';
@@ -131,6 +161,7 @@ class PhpTest extends \lithium\test\Unit {
$params = compact('key');
$result = $closure($this->Php, $params, null);
+
$this->assertIdentical($value, $result);
$key = 'non-existent';
@@ -145,11 +176,13 @@ class PhpTest extends \lithium\test\Unit {
$this->assertTrue(is_callable($closure));
$result = $closure($this->Php, array('key' => null), null);
- $expected = array('read_test' => 'value to be read', '_timestamp' => time());
+ $expected = array('read_test' => 'value to be read');
$this->assertEqual($expected, $result);
}
public function testCheck() {
+ $this->Php->read();
+
$key = 'read';
$value = 'value to be read';
$_SESSION[$key] = $value;
@@ -171,6 +204,8 @@ class PhpTest extends \lithium\test\Unit {
}
public function testDelete() {
+ $this->Php->read();
+
$key = 'delete_test';
$value = 'value to be deleted';
@@ -181,19 +216,74 @@ class PhpTest extends \lithium\test\Unit {
$params = compact('key');
$result = $closure($this->Php, $params, null);
-
$this->assertTrue($result);
$key = 'non-existent';
-
$closure = $this->Php->delete($key);
$this->assertTrue(is_callable($closure));
$params = compact('key');
$result = $closure($this->Php, $params, null);
+ $this->assertTrue($result);
+ }
- $this->assertFalse($result);
+ public function testCheckThrowException() {
+ $Php = new MockPhp(array('init' => false));
+ $this->expectException('/Could not start session./');
+ $Php->check('whatever');
+ }
+
+ public function testReadThrowException() {
+ $Php = new MockPhp(array('init' => false));
+ $this->expectException('/Could not start session./');
+ $Php->read('whatever');
+ }
+
+ public function testWriteThrowException() {
+ $Php = new MockPhp(array('init' => false));
+ $this->expectException('/Could not start session./');
+ $Php->write('whatever', 'value');
+ }
+
+ public function testDeleteThrowException() {
+ $Php = new MockPhp(array('init' => false));
+ $this->expectException('/Could not start session./');
+ $Php->delete('whatever');
+ }
+
+ public function testReadDotSyntax() {
+ $this->Php->read();
+
+ $key = 'dot';
+ $value = array('syntax' => array('key' => 'value'));
+
+ $_SESSION[$key] = $value;
+
+ $closure = $this->Php->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Php, $params, null);
+
+ $this->assertIdentical($value, $result);
+
+ $params = array('key' => 'dot.syntax');
+ $result = $closure($this->Php, $params, null);
+
+ $this->assertIdentical($value['syntax'], $result);
+ }
+
+ public function testWriteDotSyntax() {
+ $key = 'dot.syntax';
+ $value = 'value to be written';
+
+ $closure = $this->Php->write($key, $value);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'value');
+ $result = $closure($this->Php, $params, null);
+ $this->assertEqual($_SESSION['dot']['syntax'], $value);
}
}
diff --git a/libraries/lithium/tests/cases/storage/session/strategy/HmacTest.php b/libraries/lithium/tests/cases/storage/session/strategy/HmacTest.php
new file mode 100644
index 0000000..0fd4f47
--- /dev/null
+++ b/libraries/lithium/tests/cases/storage/session/strategy/HmacTest.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\storage\session\strategy;
+
+use lithium\storage\session\strategy\Hmac;
+use lithium\tests\mocks\storage\session\strategy\MockCookieSession;
+
+
+class HmacTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->secret = 'foobar';
+ $this->Hmac = new Hmac(array('secret' => $this->secret));
+ $this->mock = 'lithium\tests\mocks\storage\session\strategy\MockCookieSession';
+ MockCookieSession::reset();
+ }
+
+ public function testConstructException() {
+ $this->expectException('/HMAC strategy requires a secret key./');
+ $hmac = new Hmac();
+ }
+
+ public function testConstruct() {
+ $secret = 'foo';
+ $hmac = new Hmac(compact('secret'));
+ $this->assertTrue($hmac instanceof Hmac);
+ }
+
+ public function testWrite() {
+ $value = 'value';
+ $key = 'new_key';
+ $oldData = MockCookieSession::data();
+ $class = $this->mock;
+
+ $result = $this->Hmac->write($value, compact('key', 'class'));
+ $this->assertEqual($value, $result);
+
+ $signature = hash_hmac('sha1', serialize($oldData + array($key => $value)), $this->secret);
+ $signedData = MockCookieSession::data();
+ $this->assertEqual($signedData, $oldData + array('__signature' => $signature));
+ }
+
+ public function testReadWithValidSignature() {
+ $class = $this->mock;
+ $currentData = MockCookieSession::data();
+ $signature = hash_hmac('sha1', serialize($currentData), $this->secret);
+ $result = MockCookieSession::write('__signature', $signature);
+ $this->assertEqual($signature, $result);
+
+ $value = 'data_read';
+ $result = $this->Hmac->read($value, compact('class'));
+ $this->assertEqual($value, $result);
+ }
+
+ public function testReadWithNoSignature() {
+ $class = $this->mock;
+ $value = 'data_read';
+ $this->expectException('/HMAC signature not found./');
+ $result = $this->Hmac->read($value, compact('class'));
+ }
+
+ public function testReadWithInvalidSignature() {
+ $class = $this->mock;
+ $currentData = MockCookieSession::data();
+ $signature = 'some_invalid_signature';
+ $result = MockCookieSession::write('__signature', $signature);
+ $this->assertEqual($signature, $result);
+
+ $value = 'data_read_that_wont_match_signature';
+ $this->expectException('/Possible data tampering: HMAC signature does not match data./');
+ $result = $this->Hmac->read($value, compact('class'));
+ }
+
+ public function testDelete() {
+ $key = 'one';
+ $class = $this->mock;
+ $oldData = MockCookieSession::data();
+ $currentSignature = hash_hmac('sha1', serialize($oldData), $this->secret);
+ $result = MockCookieSession::write('__signature', $currentSignature);
+
+ $newData = $oldData;
+ unset($newData[$key]);
+
+ $expectedSignature = hash_hmac('sha1', serialize($newData), $this->secret);
+ $result = $this->Hmac->delete('foo', compact('class', 'key'));
+
+ $this->assertEqual('foo', $result);
+ $signature = MockCookieSession::read('__signature');
+ $this->assertEqual($expectedSignature, $signature);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/HelperTest.php b/libraries/lithium/tests/cases/template/HelperTest.php
index 1a4711d..1ca58e7 100644
--- a/libraries/lithium/tests/cases/template/HelperTest.php
+++ b/libraries/lithium/tests/cases/template/HelperTest.php
@@ -2,16 +2,16 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\template;
-use \stdClass;
-use \lithium\template\Helper;
-use \lithium\tests\mocks\template\MockHelper;
-use \lithium\tests\mocks\template\MockRenderer;
+use stdClass;
+use lithium\template\Helper;
+use lithium\tests\mocks\template\MockHelper;
+use lithium\tests\mocks\template\MockRenderer;
class HelperTest extends \lithium\test\Unit {
@@ -51,6 +51,15 @@ class HelperTest extends \lithium\test\Unit {
));
$expected = '<script>//alert("XSS!");</script>';
$this->assertEqual($expected, $result);
+
+ $result = $this->helper->escape(array(
+ '<script>alert("XSS!");</script>', '<script>alert("XSS!");</script>'
+ ));
+ $expected = array(
+ '<script>alert("XSS!");</script>',
+ '<script>alert("XSS!");</script>'
+ );
+ $this->assertEqual($expected, $result);
}
/**
@@ -82,10 +91,53 @@ class HelperTest extends \lithium\test\Unit {
$result = $this->helper->testAttributes($attributes);
$this->assertEqual($expected, $result);
+ $attributes = ' value="1" title="one"';
+ $result = $this->helper->testAttributes('value="1" title="one"');
+ $this->assertEqual($expected, $result);
+
$attributes = array('checked' => true, 'title' => 'one');
$expected = ' checked="checked" title="one"';
$result = $this->helper->testAttributes($attributes);
$this->assertEqual($expected, $result);
+
+ $attributes = array('checked' => false);
+ $result = $this->helper->testAttributes($attributes);
+ $this->assertEqual(' ', $result);
+ }
+
+ public function testAttributeEscaping() {
+ $attributes = array('checked' => true, 'title' => '<foo>');
+ $expected = ' checked="checked" title="<foo>"';
+ $result = $this->helper->testAttributes($attributes);
+ $this->assertEqual($expected, $result);
+
+ $attributes = array('checked' => true, 'title' => '<foo>');
+ $expected = ' checked="checked" title="<foo>"';
+ $result = $this->helper->testAttributes($attributes, null, array('escape' => false));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testAttributeMinimization() {
+ $attributes = array('selected' => 1);
+ $expected = ' selected="selected"';
+ $result = $this->helper->testAttributes($attributes);
+ $this->assertEqual($expected, $result);
+
+ $attributes = array('selected' => true);
+ $expected = ' selected="selected"';
+ $result = $this->helper->testAttributes($attributes);
+ $this->assertEqual($expected, $result);
+
+ $attributes = array('selected' => 'true');
+ $expected = ' selected="true"';
+ $result = $this->helper->testAttributes($attributes);
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInstantiationWithNoContext() {
+ $this->helper = new MockHelper();
+ $result = $this->helper->testRender(null, "foo {:bar}", array('bar' => 'baz'));
+ $this->assertEqual("foo baz", $result);
}
public function testRender() {
diff --git a/libraries/lithium/tests/cases/template/ViewTest.php b/libraries/lithium/tests/cases/template/ViewTest.php
index d73ef03..c59aef9 100644
--- a/libraries/lithium/tests/cases/template/ViewTest.php
+++ b/libraries/lithium/tests/cases/template/ViewTest.php
@@ -2,22 +2,17 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\template;
-use \lithium\template\View;
-use \lithium\g11n\catalog\adapter\Memory;
-use \lithium\template\view\adapter\Simple;
-
-class TestViewClass extends \lithium\template\View {
-
- public function renderer() {
- return $this->_config['renderer'];
- }
-}
+use lithium\template\View;
+use lithium\action\Response;
+use lithium\g11n\catalog\adapter\Memory;
+use lithium\template\view\adapter\Simple;
+use lithium\tests\mocks\template\MockView;
class ViewTest extends \lithium\test\Unit {
@@ -29,15 +24,18 @@ class ViewTest extends \lithium\test\Unit {
public function testInitialization() {
$expected = new Simple();
- $this->_view = new TestViewClass(array('renderer' => $expected));
+ $this->_view = new MockView(array('renderer' => $expected));
$result = $this->_view->renderer();
$this->assertEqual($expected, $result);
}
- public function testInitializationWithBadClasses() {
- $this->expectException('Template adapter Badness not found');
+ public function testInitializationWithBadLoader() {
+ $this->expectException("Class `Badness` of type `adapter.template.view` not found.");
new View(array('loader' => 'Badness'));
- $this->expectException('Template adapter Badness not found');
+ }
+
+ public function testInitializationWithBadRenderer() {
+ $this->expectException("Class `Badness` of type `adapter.template.view` not found.");
new View(array('renderer' => 'Badness'));
}
@@ -48,6 +46,31 @@ class ViewTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ /**
+ * Tests that the output-escaping handler correctly inherits its encoding from the `Response`
+ * object, if provided.
+ *
+ * @return void
+ */
+ public function testEscapeOutputFilterWithInjectedEncoding() {
+ $message = "Multibyte string support must be enabled to test character encodings.";
+ $this->skipIf(!function_exists('mb_convert_encoding'), $message);
+
+ $string = "Joël";
+
+ $response = new Response();
+ $response->encoding = 'UTF-8';
+ $view = new View(compact('response'));
+ $handler = $view->outputFilters['h'];
+ $this->assertTrue(mb_check_encoding($handler($string), "UTF-8"));
+
+ $response = new Response();
+ $response->encoding = 'ISO-8859-1';
+ $view = new View(compact('response'));
+ $handler = $view->outputFilters['h'];
+ $this->assertTrue(mb_check_encoding($handler($string), "ISO-8859-1"));
+ }
+
public function testBasicRenderModes() {
$view = new View(array('loader' => 'Simple', 'renderer' => 'Simple'));
@@ -63,6 +86,12 @@ class ViewTest extends \lithium\test\Unit {
$expected = "Logged in as: Cap'n Crunch.";
$this->assertEqual($expected, $result);
+ $result = $view->render('element', array('name' => "Cap'n Crunch"), array(
+ 'element' => 'Logged in as: {:name}.'
+ ));
+ $expected = "Logged in as: Cap'n Crunch.";
+ $this->assertEqual($expected, $result);
+
$xmlHeader = '<' . '?xml version="1.0" ?' . '>' . "\n";
$result = $view->render('all', array('type' => 'auth', 'success' => 'true'), array(
'layout' => $xmlHeader . "\n{:content}\n",
@@ -72,6 +101,20 @@ class ViewTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testTwoStepRenderWithVariableCapture() {
+ $view = new View(array('loader' => 'Simple', 'renderer' => 'Simple'));
+
+ $result = $view->render(
+ array(
+ array('path' => 'element', 'capture' => array('data' => 'foo')),
+ array('path' => 'template')
+ ),
+ array('name' => "Cap'n Crunch"),
+ array('element' => 'Logged in as: {:name}.', 'template' => '--{:foo}--')
+ );
+ $this->assertEqual('--Logged in as: Cap\'n Crunch.--', $result);
+ }
+
public function testFullRenderNoLayout() {
$view = new View(array('loader' => 'Simple', 'renderer' => 'Simple'));
$result = $view->render('all', array('type' => 'auth', 'success' => 'true'), array(
diff --git a/libraries/lithium/tests/cases/template/helper/FormTest.php b/libraries/lithium/tests/cases/template/helper/FormTest.php
index 7c06771..75b66e4 100644
--- a/libraries/lithium/tests/cases/template/helper/FormTest.php
+++ b/libraries/lithium/tests/cases/template/helper/FormTest.php
@@ -2,21 +2,25 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\template\helper;
-use \lithium\action\Request;
-use \lithium\net\http\Router;
-use \lithium\data\model\Record;
-use \lithium\template\helper\Form;
-use \lithium\tests\mocks\template\helper\MockFormPost;
-use \lithium\tests\mocks\template\helper\MockFormRenderer;
+use stdClass;
+use lithium\action\Request;
+use lithium\net\http\Router;
+use lithium\data\entity\Record;
+use lithium\data\entity\Document;
+use lithium\template\helper\Form;
+use lithium\tests\mocks\template\helper\MockFormPost;
+use lithium\tests\mocks\template\helper\MockFormRenderer;
class FormTest extends \lithium\test\Unit {
+ protected $_model = 'lithium\tests\mocks\template\helper\MockFormPost';
+
/**
* Test object instance.
*
@@ -46,7 +50,11 @@ class FormTest extends \lithium\test\Unit {
Router::connect('/{:controller}/{:action}/{:id}.{:type}', array('id' => null));
Router::connect('/{:controller}/{:action}/{:args}');
- $this->context = new MockFormRenderer();
+ $request = new Request();
+ $request->params = array('controller' => 'posts', 'action' => 'index');
+ $request->persist = array('controller');
+
+ $this->context = new MockFormRenderer(compact('request'));
$this->form = new Form(array('context' => $this->context));
$base = trim($this->context->request()->env('base'), '/') . '/';
@@ -64,27 +72,36 @@ class FormTest extends \lithium\test\Unit {
public function testFormCreation() {
$result = $this->form->create();
$this->assertTags($result, array(
- 'form' => array('action' => "{$this->base}posts/add", 'method' => 'POST')
+ 'form' => array('action' => "{$this->base}posts", 'method' => 'post')
));
$result = $this->form->create(null, array('method' => 'get'));
$this->assertTags($result, array(
- 'form' => array('action' => "{$this->base}posts/add", 'method' => 'GET')
+ 'form' => array('action' => "{$this->base}posts", 'method' => 'get')
));
$result = $this->form->create(null, array('type' => 'file'));
$this->assertTags($result, array('form' => array(
- 'action' => "{$this->base}posts/add",
+ 'action' => "{$this->base}posts",
'enctype' => 'multipart/form-data',
- 'method' => 'POST',
+ 'method' => 'post',
)));
- $result = $this->form->create(null, array('method' => 'GET', 'type' => 'file'));
+ $result = $this->form->create(null, array('method' => 'get', 'type' => 'file'));
$this->assertTags($result, array('form' => array(
- 'action' => "{$this->base}posts/add",
- 'method' => 'POST',
+ 'action' => "{$this->base}posts",
+ 'method' => 'post',
'enctype' => 'multipart/form-data'
)));
+
+ $result = $this->form->create(null, array('id' => 'Registration'));
+ $this->assertTags($result, array(
+ 'form' => array(
+ 'action' => "{$this->base}posts",
+ 'method' => 'post',
+ 'id' => 'Registration'
+ )
+ ));
}
/**
@@ -94,63 +111,98 @@ class FormTest extends \lithium\test\Unit {
*/
public function testRestFormCreation() {
$result = $this->form->create(null, array('action' => 'delete', 'method' => 'delete'));
- $this->assertTags($result, array('form' => array(
- 'action' => "{$this->base}posts/delete", 'method' => 'DELETE'
- )));
+
+ $this->assertTags($result, array(
+ 'form' => array(
+ 'action' => "{$this->base}posts/delete", 'method' => 'post',
+ ),
+ 'input' => array('type' => "hidden", 'name' => '_method', 'value' => 'DELETE')
+ ));
$result = $this->form->create(null, array('method' => 'put', 'type' => 'file'));
- $this->assertTags($result, array('form' => array(
- 'action' => "{$this->base}posts/add",
- 'method' => 'PUT',
- 'enctype' => 'multipart/form-data'
- )));
+ $this->assertTags($result, array(
+ 'form' => array(
+ 'action' => "{$this->base}posts",
+ 'method' => 'post',
+ 'enctype' => 'multipart/form-data'
+ ),
+ 'input' => array('type' => "hidden", 'name' => '_method', 'value' => 'PUT')
+ ));
+
+ $record = new Record(array('exists' => true, 'model' => $this->_model));
+ $result = $this->form->create($record);
+
+ $this->assertTags($result, array(
+ 'form' => array('action' => "{$this->base}posts", 'method' => 'post'),
+ 'input' => array('type' => "hidden", 'name' => '_method', 'value' => 'PUT')
+ ));
}
public function testFormCreationWithBinding() {
- $record = new Record(array(
- 'model' => 'lithium\tests\mocks\template\helper\MockFormPost',
- 'data' => array(
- 'id' => '5',
- 'author_id' => '2',
- 'title' => 'This is a saved post',
- 'body' => 'This is the body of the saved post'
- )
- ));
+ $record = new Record(array('model' => $this->_model, 'data' => array(
+ 'id' => '5',
+ 'author_id' => '2',
+ 'title' => 'This is a saved post',
+ 'body' => 'This is the body of the saved post'
+ )));
$result = $this->form->create($record);
+ $this->assertTags($result, array(
+ 'form' => array('action' => "{$this->base}posts", 'method' => 'post')
+ ));
}
- public function testFormDataBinding() {
- $record = new Record(array(
- 'model' => 'lithium\tests\mocks\template\helper\MockFormPost',
- 'data' => array(
- 'id' => '5',
- 'author_id' => '2',
- 'title' => 'This is a saved post',
- 'body' => 'This is the body of the saved post'
- )
+ /**
+ * Ensures that password fields aren't rendered with pre-populated values from bound record or
+ * document objects.
+ *
+ * @return void
+ */
+ public function testPasswordWithBindingValue() {
+ $this->form->create(new Record(array(
+ 'model' => $this->_model, 'data' => array('pass' => 'foobar')
+ )));
+ $result = $this->form->password('pass');
+
+ $this->assertTags($result, array(
+ 'input' => array('type' => 'password', 'name' => 'pass', 'id' => 'MockFormPostPass')
));
+ }
+
+ public function testFormDataBinding() {
+ $this->expectException('The data connection default is not configured');
+ MockFormPost::config(array('connection' => false));
+
+ $record = new Record(array('model' => $this->_model, 'data' => array(
+ 'id' => '5',
+ 'author_id' => '2',
+ 'title' => 'This is a saved post',
+ 'body' => 'This is the body of the saved post'
+ )));
$result = $this->form->create($record);
$this->assertTags($result, array(
- 'form' => array('action' => "{$this->base}posts/add", 'method' => 'POST')
+ 'form' => array('action' => "{$this->base}posts", 'method' => 'post')
));
$result = $this->form->text('title');
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'title', 'value' => 'This is a saved post'
+ 'type' => 'text', 'name' => 'title',
+ 'value' => 'This is a saved post', 'id' => 'MockFormPostTitle'
)));
$result = $this->form->end();
$this->assertTags($result, array('/form'));
$result = $this->form->text('title');
- $this->assertTags($result, array('input' => array('type' => 'text', 'name' => 'title')));
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'text', 'name' => 'title', 'id' => 'Title'
+ )));
}
public function testTextBox() {
$result = $this->form->text('foo');
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'foo'
+ 'type' => 'text', 'name' => 'foo', 'id' => 'Foo'
)));
}
@@ -161,21 +213,23 @@ class FormTest extends \lithium\test\Unit {
$result = $this->form->text('foo');
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'foo', 'class' => 'editable'
+ 'type' => 'text', 'name' => 'foo', 'class' => 'editable', 'id' => 'Foo'
)));
$this->form->config(array('base' => array('maxlength' => 255)));
$result = $this->form->text('foo');
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'foo', 'class' => 'editable', 'maxlength' => '255'
+ 'type' => 'text', 'name' => 'foo', 'class' => 'editable',
+ 'maxlength' => '255', 'id' => 'Foo'
)));
$this->form->config(array('text' => array('class' => 'locked')));
$result = $this->form->text('foo');
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'foo', 'class' => 'locked', 'maxlength' => '255'
+ 'type' => 'text', 'name' => 'foo', 'class' => 'locked',
+ 'maxlength' => '255', 'id' => 'Foo'
)));
$result = $this->form->config();
@@ -183,28 +237,44 @@ class FormTest extends \lithium\test\Unit {
'base' => array('class' => 'editable', 'maxlength' => 255),
'text' => array('class' => 'locked'),
'textarea' => array(),
- 'templates' => array('create' => 'form', 'end' => 'form-end')
+ 'templates' => array('create' => 'form', 'end' => 'form-end'),
+ 'attributes' => array('id' => $result['attributes']['id'])
);
$this->assertEqual($expected, $result);
+ $this->assertTrue(is_callable($result['attributes']['id']));
}
public function testFormElementWithDefaultValue() {
$result = $this->form->text('foo', array('default' => 'Message here'));
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'foo', 'value' => 'Message here'
+ 'type' => 'text', 'name' => 'foo', 'value' => 'Message here', 'id' => 'Foo'
)));
$result = $this->form->text('foo', array(
- 'default' => 'Message here', 'value' => 'My Name Is Jonas'
+ 'default' => 'Message here', 'value' => 'My Name Is Jonas', 'id' => 'Foo'
));
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'foo', 'value' => 'My Name Is Jonas'
+ 'type' => 'text', 'name' => 'foo', 'value' => 'My Name Is Jonas', 'id' => 'Foo'
)));
$result = $this->form->text('foo', array('value' => 'My Name Is Jonas'));
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'foo', 'value' => 'My Name Is Jonas'
+ 'type' => 'text', 'name' => 'foo', 'value' => 'My Name Is Jonas', 'id' => 'Foo'
+ )));
+ }
+
+ public function testFormInputField() {
+ $result = $this->form->file('upload');
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'file', 'name' => 'upload', 'id' => 'Upload'
+ )));
+ }
+
+ public function testHiddenFieldWithId() {
+ $result = $this->form->hidden('my_field');
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'hidden', 'name' => 'my_field', 'id' => 'MyField'
)));
}
@@ -222,6 +292,24 @@ class FormTest extends \lithium\test\Unit {
'User Name',
'/label'
));
+
+ $result = $this->form->label('first_name', array(
+ 'First Name' => array('id' => 'first_name_label')
+ ));
+ $this->assertTags($result, array(
+ 'label' => array('for' => 'first_name', 'id' => 'first_name_label'),
+ 'First Name',
+ '/label'
+ ));
+
+ $result = $this->form->label('first_name', array(
+ null => array('id' => 'first_name_label')
+ ));
+ $this->assertTags($result, array(
+ 'label' => array('for' => 'first_name', 'id' => 'first_name_label'),
+ 'First Name',
+ '/label'
+ ));
}
public function testLabelGenerationWithNoEscape() {
@@ -251,7 +339,7 @@ class FormTest extends \lithium\test\Unit {
public function testTextareaGeneration() {
$result = $this->form->textarea('foo', array('value' => 'some content'));
$this->assertTags($result, array(
- 'textarea' => array('name' => 'foo'),
+ 'textarea' => array('name' => 'foo', 'id' => 'Foo'),
'some content',
'/textarea'
));
@@ -259,25 +347,129 @@ class FormTest extends \lithium\test\Unit {
public function testCheckboxGeneration() {
$result = $this->form->checkbox('foo');
- $this->assertTags($result, array('input' => array('type' => 'checkbox', 'name' => 'foo')));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo', 'id' => 'Foo'
+ ))
+ ));
$result = $this->form->checkbox('foo', array('checked' => false));
- $this->assertTags($result, array('input' => array('type' => 'checkbox', 'name' => 'foo')));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo', 'id' => 'Foo'
+ ))
+ ));
$result = $this->form->checkbox('foo', array('checked' => true));
- $this->assertTags($result, array('input' => array(
- 'type' => 'checkbox', 'name' => 'foo', 'checked' => 'checked'
- )));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo',
+ 'checked' => 'checked', 'id' => 'Foo'
+ ))
+ ));
+
+ $record = new Record(array('model' => $this->_model, 'data' => array('foo' => true)));
+ $this->form->create($record);
+
+ $result = $this->form->checkbox('foo');
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo',
+ 'checked' => 'checked', 'id' => 'MockFormPostFoo'
+ ))
+ ));
+ }
+
+ public function testCustomCheckbox() {
+ $result = $this->form->checkbox('foo', array('value' => '1'));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo', 'id' => 'Foo'
+ ))
+ ));
+
+ $result = $this->form->checkbox('foo', array('checked' => true, 'value' => '1'));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo',
+ 'checked' => 'checked', 'id' => 'Foo'
+ ))
+ ));
+
+ $record = new Record(array('model' => $this->_model, 'data' => array('foo' => true)));
+ $this->form->create($record);
+
+ $result = $this->form->checkbox('foo', array('value' => '1'));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo',
+ 'id' => 'MockFormPostFoo', 'checked' => 'checked'
+ ))
+ ));
$result = $this->form->checkbox('foo', array('value' => true));
- $this->assertTags($result, array('input' => array(
- 'type' => 'checkbox', 'name' => 'foo', 'checked' => 'checked'
- )));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => '1', 'name' => 'foo', 'id' => 'MockFormPostFoo'
+ ))
+ ));
+ }
+
+ public function testCustomValueCheckbox() {
+ $result = $this->form->checkbox('foo', array('value' => 'HERO'));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => 'HERO', 'name' => 'foo', 'id' => 'Foo'
+ ))
+ ));
+
+ $result = $this->form->checkbox('foo', array('value' => 'nose'));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => 'nose', 'name' => 'foo', 'id' => 'Foo'
+ ))
+ ));
+
+ $record = new Record(array('model' => $this->_model, 'data' => array('foo' => 'nose')));
+ $record->foo = 'nose';
+ $this->form->create($record);
+
+ $result = $this->form->checkbox('foo', array('value' => 'nose'));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => 'nose', 'name' => 'foo', 'id' => 'MockFormPostFoo'
+ ))
+ ));
+
+ $record = new Record(array('model' => $this->_model, 'data' => array('foo' => 'foot')));
+ $this->form->create($record);
+
+ $result = $this->form->checkbox('foo', array('value' => 'nose'));
+ $this->assertTags($result, array(
+ array('input' => array('type' => 'hidden', 'value' => '', 'name' => 'foo')),
+ array('input' => array(
+ 'type' => 'checkbox', 'value' => 'nose', 'name' => 'foo',
+ 'checked' => 'checked', 'id' => 'MockFormPostFoo'
+ ))
+ ));
}
public function testSelectGeneration() {
$result = $this->form->select('foo');
- $this->assertTags($result, array('select' => array('name' => 'foo'), '/select'));
+ $this->assertTags($result, array(
+ 'select' => array('name' => 'foo', 'id' => 'Foo'), '/select'
+ ));
$result = $this->form->select(
'colors',
@@ -306,7 +498,7 @@ class FormTest extends \lithium\test\Unit {
));
$this->assertTags($result, array(
- 'select' => array('name' => 'numbers'),
+ 'select' => array('id' => 'Numbers', 'name' => 'numbers'),
array('option' => array('value' => '', 'selected' => 'selected')),
'/option',
array('option' => array('value' => '1')),
@@ -323,7 +515,7 @@ class FormTest extends \lithium\test\Unit {
));
$this->assertTags($result, array(
- 'select' => array('name' => 'numbers'),
+ 'select' => array('name' => 'numbers', 'id' => 'Numbers'),
array('option' => array('value' => '', 'selected' => 'selected')),
'> Make a selection',
'/option',
@@ -340,20 +532,20 @@ class FormTest extends \lithium\test\Unit {
public function testTemplateRemapping() {
$result = $this->form->password('passwd');
$this->assertTags($result, array('input' => array(
- 'type' => 'password', 'name' => 'passwd'
+ 'type' => 'password', 'name' => 'passwd', 'id' => 'Passwd'
)));
$this->form->config(array('templates' => array('password' => 'text')));
$result = $this->form->password('passwd');
$this->assertTags($result, array('input' => array(
- 'type' => 'text', 'name' => 'passwd'
+ 'type' => 'text', 'name' => 'passwd', 'id' => 'Passwd'
)));
}
public function testMultiSelect() {
$expected = array(
- 'select' => array('name' => 'numbers[]', 'multiple' => 'multiple'),
+ 'select' => array('name' => 'numbers[]', 'id' => 'Numbers', 'multiple' => 'multiple'),
array('option' => array('value' => '', 'selected' => 'selected')),
'> Make a selection',
'/option',
@@ -372,7 +564,9 @@ class FormTest extends \lithium\test\Unit {
$this->assertTags($result, $expected);
$expected = array(
- 'select' => array('name' => 'numbers[]', 'multiple' => 'multiple', 'size' => 5),
+ 'select' => array(
+ 'name' => 'numbers[]', 'multiple' => 'multiple', 'size' => 5, 'id' => 'Numbers'
+ ),
array('option' => array('value' => '1')),
'first',
'/option',
@@ -390,7 +584,7 @@ class FormTest extends \lithium\test\Unit {
public function testMultiselected() {
$expected = array(
- 'select' => array('name' => 'numbers[]', 'multiple' => 'multiple'),
+ 'select' => array('name' => 'numbers[]', 'id' => 'Numbers', 'multiple' => 'multiple'),
array('option' => array('value' => '1', 'selected' => 'selected')),
'first',
'/option',
@@ -405,12 +599,9 @@ class FormTest extends \lithium\test\Unit {
'/option',
'/select'
);
- $result = $this->form->select('numbers', array(
- 1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth'
- ),array(
- 'value' => array(1,3,4),
- 'multiple' => true
- ));
+ $list = array(1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth');
+ $options = array('value' => array(1, 3, 4), 'multiple' => true);
+ $result = $this->form->select('numbers', $list, $options);
$this->assertTags($result, $expected);
}
@@ -421,29 +612,170 @@ class FormTest extends \lithium\test\Unit {
$form = new Form(compact('context'));
$result = $form->create();
- $this->assertTags($result, array(
- 'form' => array('action' => "{$this->base}mock/test/1", 'method' => 'POST')
- ));
+ $this->assertTags($result, array('form' => array(
+ 'action' => "{$this->base}mock/test/1",
+ 'method' => 'post',
+ )));
}
public function testFormCreateWithMoreParamsButSpecifiedAction() {
$request = new Request();
$request->params = array('controller' => 'mock', 'action' => 'test', 'args' => array('1'));
+ $request->persist = array('controller');
$context = new MockFormRenderer(compact('request'));
$form = new Form(compact('context'));
$result = $form->create(null, array('action' => 'radness'));
- $this->assertTags($result, array(
- 'form' => array('action' => "{$this->base}mock/radness", 'method' => 'POST')
- ));
+ $this->assertTags($result, array('form' => array(
+ 'action' => "{$this->base}mock/radness",
+ 'method' => 'post',
+ )));
}
public function testFormField() {
$result = $this->form->field('name');
$this->assertTags($result, array(
'div' => array(),
- 'label' => array('for' => 'name'), 'Name', '/label',
- 'input' => array('type' => 'text', 'name' => 'name'),
+ 'label' => array('for' => 'Name'), 'Name', '/label',
+ 'input' => array('type' => 'text', 'name' => 'name', 'id' => 'Name'),
+ '/div'
+ ));
+
+ $result = $this->form->field('name', array('type' => 'radio', 'value' => 'foo'));
+ $this->assertTags($result, array(
+ 'div' => array(),
+ 'input' => array('type' => 'radio', 'name' => 'name', 'value' => 'foo', 'id' => 'Name'),
+ 'label' => array('for' => 'Name'), 'Name', '/label',
+ '/div'
+ ));
+
+ $result = $this->form->field('name', array('type' => 'checkbox'));
+ $expected = array(
+ '<div>',
+ '<input type="hidden" name="name" value="" />',
+ '<input type="checkbox" name="name" id="Name" value="1" />',
+ '<label for="Name">Name</label></div>'
+ );
+ $this->assertEqual(join('', $expected), $result);
+ }
+
+ /**
+ * Verifies that calls to `field()` with `'type' => 'hidden'` do not produce `<label />`s.
+ *
+ * @return void
+ */
+ public function testHiddenFieldWithNoLabel() {
+ $result = $this->form->field('foo', array('type' => 'hidden'));
+ $this->assertTags($result, array(
+ 'div' => array(),
+ 'input' => array('type' => 'hidden', 'name' => 'foo', 'id' => 'Foo'),
+ '/div'
+ ));
+ }
+
+ public function testFormFieldWithCustomTemplate() {
+ $result = $this->form->field('name', array(
+ 'template' => '<div{:wrap}>{:label}: {:input}{:error}</div>'
+ ));
+ $this->assertTags($result, array(
+ 'div' => array(),
+ 'label' => array('for' => 'Name'), 'Name', '/label', ':',
+ 'input' => array('type' => 'text', 'name' => 'name', 'id' => 'Name'),
+ ));
+ }
+
+ public function testFieldWithLabelShorthand() {
+ $result = $this->form->field(array('name' => 'Enter a name'));
+ $this->assertTags($result, array(
+ 'div' => array(),
+ 'label' => array('for' => 'Name'), 'Enter a name', '/label',
+ 'input' => array('type' => 'text', 'name' => 'name', 'id' => 'Name'),
+ ));
+ }
+
+ /**
+ * Demonstrates that the options for a `<label />` element can be passed through the `field()`
+ * method, using the label text as a key.
+ *
+ * @return void
+ */
+ public function testFieldLabelWithOptions() {
+ $result = $this->form->field('name', array(
+ 'label' => array('Item Name' => array('class' => 'required'))
+ ));
+ $this->assertTags($result, array(
+ 'div' => array(),
+ 'label' => array('for' => 'Name', 'class' => 'required'), 'Item Name', '/label',
+ 'input' => array('type' => 'text', 'name' => 'name', 'id' => 'Name'),
+ ));
+
+ $result = $this->form->field('video_preview', array(
+ 'label' => array('<a href="http://www.youtube.com/">Youtube</a>' => array(
+ 'escape' => false
+ ))
+ ));
+ $this->assertTags($result, array(
+ 'div' => array(),
+ 'label' => array('for' => 'VideoPreview'),
+ 'a' => array('href' => 'http://www.youtube.com/'), 'Youtube', '/a', '/label',
+ 'input' => array('type' => 'text', 'name' => 'video_preview', 'id' => 'VideoPreview'),
+ ));
+ }
+
+ public function testMultipleFields() {
+ $result = $this->form->field(array(
+ 'name' => 'Enter a name',
+ 'phone_number',
+ 'email' => 'Enter a valid email'
+ ));
+ $this->assertTags($result, array(
+ array('div' => array()),
+ array('label' => array('for' => 'Name')),
+ 'Enter a name',
+ '/label',
+ array('input' => array('type' => 'text', 'name' => 'name', 'id' => 'Name')),
+ '/div',
+
+ array('div' => array()),
+ array('label' => array('for' => 'PhoneNumber')),
+ 'Phone Number',
+ '/label',
+ array('input' => array(
+ 'type' => 'text', 'name' => 'phone_number', 'id' => 'PhoneNumber'
+ )),
+ '/div',
+
+ array('div' => array()),
+ array('label' => array('for' => 'Email')),
+ 'Enter a valid email',
+ '/label',
+ array('input' => array('type' => 'text', 'name' => 'email', 'id' => 'Email')),
+ '/div'
+ ));
+ }
+
+ public function testCustomInputTypes() {
+ // Creates an HTML5 'range' input slider:
+ $range = $this->form->range('completion', array('min' => 0, 'max' => 100));
+ $this->assertTags($range, array('input' => array(
+ 'type' => 'range', 'name' => 'completion',
+ 'min' => '0', 'max' => '100', 'id' => 'Completion'
+ )));
+ }
+
+ public function testFieldWithCustomType() {
+ $field = $this->form->field('completion', array(
+ 'type' => 'range', 'id' => 'completion', 'min' => '0', 'max' => '100',
+ 'label' => 'Completion %', 'wrap' => array('class' => 'input')
+ ));
+ $this->assertTags($field, array(
+ 'div' => array('class' => 'input'),
+ 'label' => array('for' => 'completion'), 'Completion %', '/label',
+ 'input' => array(
+ 'type' => 'range', 'name' => 'completion',
+ 'id' => 'completion', 'min' => '0', 'max' => '100'
+ ),
+ '/div'
));
}
@@ -453,8 +785,8 @@ class FormTest extends \lithium\test\Unit {
));
$this->assertTags($result, array(
'div' => array(),
- 'label' => array('for' => 'states'), 'States', '/label',
- 'select' => array('name' => 'states'),
+ 'label' => array('for' => 'States'), 'States', '/label',
+ 'select' => array('name' => 'states', 'id' => 'States'),
array('option' => array('value' => '0', 'selected' => 'selected')),
'CA',
'/option',
@@ -472,36 +804,86 @@ class FormTest extends \lithium\test\Unit {
}
public function testFormErrorWithRecordAndStringError() {
- $record = new Record();
+ $record = new Record(array('model' => $this->_model));
$record->errors(array('name' => 'Please enter a name'));
$this->form->create($record);
$result = $this->form->error('name');
$this->assertTags($result, array(
- 'div' => array(), 'Please enter a name', '/div'
+ 'div' => array('class' => 'error'), 'Please enter a name', '/div'
+ ));
+ }
+
+ public function testFormMultipleErrors() {
+ $record = new Record(array('model' => $this->_model));
+ $record->errors(array('email' => array('Empty', 'Valid')));
+ $this->form->create($record);
+
+ $result = $this->form->error('email');
+ $this->assertTags($result, array(
+ array('div' => array('class' => 'error')), 'Empty', '/div',
+ array('div' => array('class' => 'error')), 'Valid', '/div'
));
+
+ $result = $this->form->error('email', 0);
+ $this->assertTags($result, array('div' => array('class' => 'error'), 'Empty', '/div'));
+
+ $result = $this->form->error('email', 1);
+ $this->assertTags($result, array('div' => array('class' => 'error'), 'Valid', '/div'));
+
+ $result = $this->form->error('email', true);
+ $this->assertTags($result, array('div' => array('class' => 'error'), 'Empty', '/div'));
}
public function testFormErrorWithRecordAndSpecificKey() {
- $record = new Record();
+ $record = new Record(array('model' => $this->_model));
$record->errors(array('name' => array('Please enter a name')));
$this->form->create($record);
$result = $this->form->error('name', 0);
$this->assertTags($result, array(
- 'div' => array(), 'Please enter a name', '/div'
+ 'div' => array('class' => 'error'), 'Please enter a name', '/div'
+ ));
+ }
+
+ public function testFormErrorWithRecordAndSpecificKeyAndValue() {
+ $record = new Record(array('model' => $this->_model));
+ $record->name = 'Nils';
+ $record->errors(array('name' => array('Please enter a name')));
+ $this->form->create($record);
+
+ $result = $this->form->error('name');
+ $this->assertTags($result, array(
+ 'div' => array('class' => 'error'), 'Please enter a name', '/div'
));
}
public function testFormFieldWithError() {
- $record = new Record();
+ $record = new Record(array('model' => $this->_model));
$record->errors(array('name' => array('Please enter a name')));
$this->form->create($record);
- $expected = '<div><label for="name">Name</label><input type="text" name="name" />'
- . '<div>Please enter a name</div></div>';
$result = $this->form->field('name');
- $this->assertEqual($expected, $result);
+ $this->assertTags($result, array(
+ '<div', 'label' => array('for' => 'MockFormPostName'), 'Name', '/label',
+ 'input' => array('type' => "text", 'name' => 'name', 'id' => 'MockFormPostName'),
+ 'div' => array('class' => "error"), 'Please enter a name', '/div', '/div'
+ ));
+ }
+
+ public function testErrorWithCustomConfiguration() {
+ $this->form->config(array('error' => array('class' => 'custom-error-class')));
+
+ $record = new Record(array('model' => $this->_model));
+ $record->errors(array('name' => array('Please enter a name')));
+ $this->form->create($record);
+
+ $result = $this->form->field('name');
+ $this->assertTags($result, array(
+ '<div', 'label' => array('for' => 'MockFormPostName'), 'Name', '/label',
+ 'input' => array('type' => "text", 'name' => 'name', 'id' => 'MockFormPostName'),
+ 'div' => array('class' => "custom-error-class"), 'Please enter a name', '/div', '/div'
+ ));
}
/**
@@ -513,10 +895,67 @@ class FormTest extends \lithium\test\Unit {
$this->form->config(array('templates' => array('field' => '{:label}{:input}{:error}')));
$result = $this->form->field('name', array('type' => 'text'));
$this->assertTags($result, array(
- 'label' => array('for' => 'name'), 'Name', '/label',
- 'input' => array('type' => 'text', 'name' => 'name')
+ 'label' => array('for' => 'Name'), 'Name', '/label',
+ 'input' => array('type' => 'text', 'name' => 'name', 'id' => 'Name')
+ ));
+ }
+
+ public function testFieldAssumeSelectIfList() {
+ $result = $this->form->field('colors', array(
+ 'list' => array('r' => 'red', 'g' => 'green', 'b' => 'blue')
+ ));
+ $expected = array(
+ '<div',
+ array('label' => array('for' => 'Colors')),
+ 'Colors',
+ '/label',
+ 'select' => array('name' => 'colors', 'id' => 'Colors'),
+ array('option' => array('value' => 'r')),
+ 'red',
+ '/option',
+ array('option' => array('value' => 'g')),
+ 'green',
+ '/option',
+ array('option' => array('value' => 'b')),
+ 'blue',
+ '/option',
+ '/select',
+ '/div'
+ );
+ $this->assertTags($result, $expected);
+ }
+
+ public function testFieldInputIdWithFormId() {
+ $this->form->create(null, array('id' => 'registration'));
+ $result = $this->form->field('name');
+
+ $this->assertTags($result, array(
+ 'div' => array(),
+ 'label' => array('for' => 'Name'), 'Name', '/label',
+ 'input' => array('type' => 'text', 'name' => 'name', 'id' => 'Name'),
));
}
+
+ /**
+ * Tests that inputs for nested objects can be assigned using dot syntax.
+ *
+ * @return void
+ */
+ public function testNestedFieldAccess() {
+ $doc = new Document(array('data' => array('foo' => array('bar' => 'value'))));
+ $this->form->create($doc);
+
+ $result = $this->form->text('foo.bar');
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'text', 'name' => 'foo[bar]', 'id' => 'FooBar', 'value' => 'value'
+ )));
+ }
+
+ public function testFormCreationWithNoContext() {
+ $this->form = new Form(array('context' => new MockFormRenderer()));
+ $result = $this->form->create(null, array('url' => '/foo'));
+ $this->assertTags($result, array('form' => array('action' => "/foo", 'method'=> "post")));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/helper/HtmlTest.php b/libraries/lithium/tests/cases/template/helper/HtmlTest.php
index 4c9d5d3..9f20dad 100644
--- a/libraries/lithium/tests/cases/template/helper/HtmlTest.php
+++ b/libraries/lithium/tests/cases/template/helper/HtmlTest.php
@@ -2,15 +2,17 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\template\helper;
-use \lithium\net\http\Router;
-use \lithium\template\helper\Html;
-use \lithium\tests\mocks\template\helper\MockHtmlRenderer;
+use lithium\net\http\Router;
+use lithium\template\helper\Html;
+use lithium\action\Request;
+use lithium\action\Response;
+use lithium\tests\mocks\template\helper\MockHtmlRenderer;
class HtmlTest extends \lithium\test\Unit {
@@ -34,7 +36,12 @@ class HtmlTest extends \lithium\test\Unit {
Router::connect('/{:controller}/{:action}/{:id}.{:type}');
Router::connect('/{:controller}/{:action}.{:type}');
- $this->context = new MockHtmlRenderer();
+ $this->context = new MockHtmlRenderer(array(
+ 'request' => new Request(array(
+ 'base' => '', 'env' => array('HTTP_HOST' => 'foo.local')
+ )),
+ 'response' => new Response()
+ ));
$this->html = new Html(array('context' => &$this->context));
}
@@ -53,21 +60,25 @@ class HtmlTest extends \lithium\test\Unit {
}
/**
- * Tests that character set declarations render the correct character set and meta tag.
+ * Tests that character set declarations render the
+ * correct character set and short meta tag.
*
* @return void
*/
public function testCharset() {
$result = $this->html->charset();
+ $this->assertTags($result, array('meta' => array(
+ 'charset' => 'UTF-8'
+ )));
+ $result = $this->html->charset('utf-8');
$this->assertTags($result, array('meta' => array(
- 'http-equiv' => 'Content-Type', 'content' => 'text/html; charset=utf-8'
+ 'charset' => 'utf-8'
)));
$result = $this->html->charset('UTF-7');
-
$this->assertTags($result, array('meta' => array(
- 'http-equiv' => 'Content-Type', 'content' => 'text/html; charset=UTF-7'
+ 'charset' => 'UTF-7'
)));
}
@@ -280,23 +291,18 @@ class HtmlTest extends \lithium\test\Unit {
'<\/script>\s*$/',
$result
);
- }
- /**
- * Tests generating images with links wrapping them
- *
- * @return void
- */
- public function testImageLinking() {
- $this->skipIf(true, "Not implemented");
+ $result = $this->html->script("foo", array(
+ 'async' => true, 'defer' => true, 'onload' => 'init()'
+ ));
- $result = $this->html->image('test.gif', array('url' => '#'));
- $expected = array(
- 'a' => array('href' => '#'),
- 'img' => array('src' => 'regex:/img\/test\.gif\?\d*/', 'alt' => ''),
- '/a'
- );
- $this->assertTags($result, $expected);
+ $this->assertTags($result, array('script' => array(
+ 'type' => 'text/javascript',
+ 'src' => '/js/foo.js',
+ 'async' => 'async',
+ 'defer' => 'defer',
+ 'onload' => 'init()'
+ )));
}
/**
@@ -345,6 +351,21 @@ class HtmlTest extends \lithium\test\Unit {
$expected['link']['href'] = 'regex:/http:\/\/.*\/screen\.css\?1234/';
$this->assertTags($result, $expected);
}
+ /**
+ * Tests generating random tags for the <head> section
+ *
+ * @return void
+ */
+ public function testHead() {
+ $result = $this->html->head('meta', array('options' => array('author' => 'foo')));
+ $expected = array('meta' => array('author' => 'foo'));
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->head('unexisting-name', array(
+ 'options' => array('author' => 'foo')
+ ));
+ $this->assertNull($result);
+ }
/**
* Tests generating multiple <link /> or <style /> tags in a single call with an array
diff --git a/libraries/lithium/tests/cases/template/view/CompilerTest.php b/libraries/lithium/tests/cases/template/view/CompilerTest.php
index d263ff9..cd37047 100644
--- a/libraries/lithium/tests/cases/template/view/CompilerTest.php
+++ b/libraries/lithium/tests/cases/template/view/CompilerTest.php
@@ -2,27 +2,34 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\template\view;
-use \lithium\template\view\Compiler;
+use lithium\core\Libraries;
+use lithium\template\view\Compiler;
class CompilerTest extends \lithium\test\Unit {
protected $_path;
- protected $_file = 'resources/tmp/tests/template.html.php';
+ protected $_file = 'template.html.php';
public function skip() {
- $path = LITHIUM_APP_PATH . '/resources/tmp/tests';
- $this->skipIf(!is_writable($path), "{$path} is not writable.");
+ $path = realpath(Libraries::get(true, 'resources') . '/tmp/tests');
+ $this->skipIf(!is_writable($path), "Path `{$path}` is not writable.");
+
+ $path = realpath(Libraries::get(true, 'resources') . '/tmp/cache/templates');
+ $this->skipIf(!is_writable($path), "Path `{$path}` is not writable.");
}
public function setUp() {
- $this->_path = str_replace('\\', '/', LITHIUM_APP_PATH);
+ $this->_path = realpath(
+ str_replace('\\', '/', Libraries::get(true, 'resources')) . '/tmp/tests'
+ );
+
file_put_contents("{$this->_path}/{$this->_file}", "
<?php echo 'this is unescaped content'; ?" . ">
<?='this is escaped content'; ?" . ">
@@ -36,11 +43,14 @@ class CompilerTest extends \lithium\test\Unit {
that breaks over
several lines
'; ?" . ">
+ <?=\$h('This is pre-escaped content'); ?>
");
}
public function tearDown() {
- foreach (glob("{$this->_path}/resources/tmp/cache/templates/*.php") as $file) {
+ $path = realpath(Libraries::get(true, 'resources') . '/tmp/cache/templates');
+
+ foreach (glob("{$path}/*.php") as $file) {
unlink($file);
}
unlink("{$this->_path}/{$this->_file}");
@@ -48,46 +58,25 @@ class CompilerTest extends \lithium\test\Unit {
public function testTemplateContentRewriting() {
$template = Compiler::template("{$this->_path}/{$this->_file}");
-
$this->assertTrue(file_exists($template));
+ $expected = array(
+ "<?php echo 'this is unescaped content'; ?" . ">",
+ "<?php echo \$h('this is escaped content'); ?" . ">",
+ "<?php echo \$h(\$alsoEscaped); ?" . ">",
+ "<?php echo \$this->escape('this is also escaped content'); ?" . ">",
+ '<?php echo $this->escape(',
+ "'this, too, is escaped content'",
+ '); ?>',
+ "<?php echo \$h('This is",
+ 'escaped content',
+ 'that breaks over',
+ 'several lines',
+ "'); ?>",
+ "<?php echo \$h('This is pre-escaped content'); ?>"
+ );
$result = array_map('trim', explode("\n", trim(file_get_contents($template))));
-
- $expected = "<?php echo 'this is unescaped content'; ?" . ">";
- $this->assertEqual($expected, $result[0]);
-
- $expected = "<?php echo \$h('this is escaped content'); ?" . ">";
- $this->assertEqual($expected, $result[1]);
-
- $expected = "<?php echo \$h(\$alsoEscaped); ?" . ">";
- $this->assertEqual($expected, $result[2]);
-
- $expected = "<?php echo \$this->escape('this is also escaped content'); ?" . ">";
- $this->assertEqual($expected, $result[3]);
-
- $expected = '<?php echo $this->escape(';
- $this->assertEqual($expected, $result[4]);
-
- $expected = "'this, too, is escaped content'";
- $this->assertEqual($expected, $result[5]);
-
- $expected = '); ?>';
- $this->assertEqual($expected, $result[6]);
-
- $expected = "<?php echo \$h('This is";
- $this->assertEqual($expected, $result[7]);
-
- $expected = 'escaped content';
- $this->assertEqual($expected, $result[8]);
-
- $expected = 'that breaks over';
- $this->assertEqual($expected, $result[9]);
-
- $expected = 'several lines';
- $this->assertEqual($expected, $result[10]);
-
- $expected = "'); ?>";
- $this->assertEqual($expected, $result[11]);
+ $this->assertEqual($expected, $result);
}
public function testFallbackWithNonWritableDirectory() {
@@ -107,7 +96,7 @@ class CompilerTest extends \lithium\test\Unit {
}
public function testTemplateCacheHit() {
- $path = LITHIUM_APP_PATH . '/resources/tmp/cache/templates';
+ $path = Libraries::get(true, 'resources') . '/tmp/cache/templates';
$original = Compiler::template("{$this->_path}/{$this->_file}", compact('path'));
$cache = glob("{$path}/*");
clearstatcache();
diff --git a/libraries/lithium/tests/cases/template/view/RendererTest.php b/libraries/lithium/tests/cases/template/view/RendererTest.php
index d7f0383..044d2de 100644
--- a/libraries/lithium/tests/cases/template/view/RendererTest.php
+++ b/libraries/lithium/tests/cases/template/view/RendererTest.php
@@ -2,18 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\template\view;
-use \lithium\template\View;
-use \lithium\action\Request;
-use \lithium\template\Helper;
-use \lithium\template\helper\Html;
-use \lithium\template\view\adapter\Simple;
-use \lithium\net\http\Router;
+use stdClass;
+use lithium\template\View;
+use lithium\action\Request;
+use lithium\action\Response;
+use lithium\template\Helper;
+use lithium\template\helper\Html;
+use lithium\template\view\adapter\Simple;
+use lithium\net\http\Router;
class RendererTest extends \lithium\test\Unit {
@@ -21,7 +23,12 @@ class RendererTest extends \lithium\test\Unit {
$this->_routes = Router::get();
Router::reset();
Router::connect('/{:controller}/{:action}');
- $this->subject = new Simple();
+ $this->subject = new Simple(array(
+ 'request' => new Request(array(
+ 'base' => '', 'env' => array('HTTP_HOST' => 'foo.local')
+ )),
+ 'response' => new Response()
+ ));
}
public function tearDown() {
@@ -32,18 +39,19 @@ class RendererTest extends \lithium\test\Unit {
}
public function testInitialization() {
- $expected = array('url', 'path', 'options', 'content', 'title', 'scripts', 'styles');
+ $expected = array('url', 'path', 'options', 'title', 'scripts', 'styles', 'head');
$result = array_keys($this->subject->handlers());
$this->assertEqual($expected, $result);
- $expected = array('content', 'title', 'scripts', 'styles');
+ $expected = array('content', 'title', 'scripts', 'styles', 'head');
$result = array_keys($this->subject->context());
$this->assertEqual($expected, $result);
}
public function testContextQuerying() {
$expected = array(
- 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array()
+ 'content' => '', 'title' => '', 'scripts' => array(),
+ 'styles' => array(), 'head' => array()
);
$this->assertEqual($expected, $this->subject->context());
$this->assertEqual('', $this->subject->context('title'));
@@ -52,6 +60,12 @@ class RendererTest extends \lithium\test\Unit {
$this->assertNull($this->subject->foo());
$this->assertFalse(isset($this->subject->foo));
+ $result = $this->subject->title("<script>alert('XSS');</script>");
+ $this->assertEqual('<script>alert('XSS');</script>', $result);
+
+ $result = $this->subject->title();
+ $this->assertEqual('<script>alert('XSS');</script>', $result);
+
$this->subject = new Simple(array('context' => array(
'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array(), 'foo' => '!'
)));
@@ -76,6 +90,7 @@ class RendererTest extends \lithium\test\Unit {
$class = get_class($helper);
$path = $this->subject->applyHandler($helper, "{$class}::script", 'path', 'foo/file');
$this->assertEqual('/js/foo/file.js', $path);
+ $this->assertEqual('/some/generic/path', $this->subject->path('some/generic/path'));
}
public function testHandlerInsertion() {
@@ -85,9 +100,7 @@ class RendererTest extends \lithium\test\Unit {
$foo = function($value) { return "Foo: {$value}"; };
- $expected = array(
- 'url', 'path', 'options', 'content', 'title', 'scripts', 'styles', 'foo'
- );
+ $expected = array('url', 'path', 'options', 'title', 'scripts', 'styles', 'head', 'foo');
$result = array_keys($this->subject->handlers(compact('foo')));
$this->assertEqual($expected, $result);
@@ -173,9 +186,60 @@ class RendererTest extends \lithium\test\Unit {
}
public function testGetters() {
- $this->assertNull($this->subject->request());
- $this->subject = new Simple(array('request' => new Request()));
$this->assertTrue($this->subject->request() instanceof Request);
+ $this->assertTrue($this->subject->response() instanceof Response);
+ $this->subject = new Simple();
+ $this->assertNull($this->subject->request());
+ $this->assertNull($this->subject->response());
+ }
+
+ public function testSetAndData() {
+ $data = array('one' => 1, 'two' => 2, 'three' => 'value');
+ $result = $this->subject->set($data);
+ $this->assertNull($result);
+
+ $result = $this->subject->data();
+ $this->assertEqual($data, $result);
+
+ $result = $this->subject->set(array('more' => new stdClass()));
+ $this->assertNull($result);
+
+ $result = $this->subject->data();
+ $this->assertEqual($data + array('more' => new stdClass()), $result);
+ }
+
+ /**
+ * @todo Add integration test for Renderer being composed with a view object.
+ */
+ public function testView() {
+ $result = $this->subject->view();
+ $this->assertNull($result);
+ }
+
+ public function testHandlers() {
+ $this->assertTrue($this->subject->url());
+ $this->assertPattern('/\/posts\/foo/', $this->subject->url('Posts::foo'));
+
+ $absolute = $this->subject->url('Posts::foo', array('absolute' => true));
+ $this->assertEqual('http://foo.local/posts/foo', $absolute);
+
+ $this->assertFalse(trim($this->subject->scripts()));
+ $this->assertEqual('foobar', trim($this->subject->scripts('foobar')));
+ $this->assertEqual('foobar', trim($this->subject->scripts()));
+
+ $this->assertFalse(trim($this->subject->styles()));
+ $this->assertEqual('foobar', trim($this->subject->styles('foobar')));
+ $this->assertEqual('foobar', trim($this->subject->styles()));
+
+ $this->assertFalse($this->subject->title());
+ $this->assertEqual('Foo', $this->subject->title('Foo'));
+ $this->assertEqual('Bar', $this->subject->title('Bar'));
+ $this->assertEqual('Bar', $this->subject->title());
+
+ $this->assertFalse(trim($this->subject->head()));
+ $this->assertEqual('foo', trim($this->subject->head('foo')));
+ $this->assertEqual("foo\n\tbar", trim($this->subject->head('bar')));
+ $this->assertEqual("foo\n\tbar", trim($this->subject->head()));
}
}
diff --git a/libraries/lithium/tests/cases/template/view/adapter/FileTest.php b/libraries/lithium/tests/cases/template/view/adapter/FileTest.php
new file mode 100644
index 0000000..5ecaa3b
--- /dev/null
+++ b/libraries/lithium/tests/cases/template/view/adapter/FileTest.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\template\view\adapter;
+
+use lithium\core\Libraries;
+use lithium\template\view\adapter\File;
+
+class FileTest extends \lithium\test\Unit {
+
+ protected $_path;
+
+ public function setUp() {
+ $this->_path = Libraries::get(true, 'resources') . '/tmp/tests';
+
+ $template1 = '<' . '?php echo $foo; ?' . '>';
+ $template2 = '<' . '?php echo $this["foo"]; ?' . '>';
+ file_put_contents("{$this->_path}/template1.html.php", $template1);
+ file_put_contents("{$this->_path}/template2.html.php", $template2);
+ }
+
+ public function tearDown() {
+ unlink("{$this->_path}/template1.html.php");
+ unlink("{$this->_path}/template2.html.php");
+ }
+
+ public function testRenderingWithExtraction() {
+ $file = new File();
+
+ $content = $file->render("{$this->_path}/template1.html.php", array('foo' => 'bar'));
+ $this->assertEqual('bar', $content);
+
+ $content = $file->render("{$this->_path}/template2.html.php", array('foo' => 'bar'));
+ $this->assertEqual('bar', $content);
+ }
+
+ public function testRenderingWithNoExtraction() {
+ $file = new File(array('extract' => false));
+ $this->expectException('Undefined variable: foo');
+ $content = $file->render("{$this->_path}/template1.html.php", array('foo' => 'bar'));
+ $this->assertFalse($content);
+
+ $content = $file->render("{$this->_path}/template2.html.php", array('foo' => 'bar'));
+ $this->assertEqual('bar', $content);
+ }
+
+ public function testContextOffsetManipulation() {
+ $file = new File();
+ $this->assertFalse(isset($file['title']));
+
+ $file['title'] = 'Document Title';
+ $this->assertEqual('Document Title', $file['title']);
+ $this->assertTrue(isset($file['title']));
+
+ unset($file['title']);
+ $this->assertFalse(isset($file['title']));
+ }
+
+ public function testTemplateLocating() {
+ $file = new File(array('paths' => array(
+ 'template' => '{:library}/views/{:controller}/{:template}.{:type}.php'
+ )));
+
+ $template = $file->template('template', array(
+ 'controller' => 'pages', 'template' => 'home', 'type' => 'html'
+ ));
+ $this->assertPattern('/template_views_pages_home\.html_[0-9]+/', $template);
+
+ $file = new File(array('compile' => false, 'paths' => array(
+ 'template' => '{:library}/views/{:controller}/{:template}.{:type}.php'
+ )));
+ $template = $file->template('template', array(
+ 'controller' => 'pages', 'template' => 'home', 'type' => 'html'
+ ));
+ $this->assertPattern('/\/views\/pages\/home\.html\.php$/', $template);
+
+ $this->expectException('/Template not found/');
+ $file->template('template', array(
+ 'controller' => 'pages', 'template' => 'foo', 'type' => 'html'
+ ));
+ }
+
+ public function testInvalidTemplateType() {
+ $file = new File(array('compile' => false, 'paths' => array(
+ 'template' => '{:library}/views/{:controller}/{:template}.{:type}.php'
+ )));
+
+ $this->expectException("Invalid template type 'invalid'.");
+ $template = $file->template('invalid', array('template' => 'foo'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/view/adapter/SimpleTest.php b/libraries/lithium/tests/cases/template/view/adapter/SimpleTest.php
index d3dbf91..9158ddd 100644
--- a/libraries/lithium/tests/cases/template/view/adapter/SimpleTest.php
+++ b/libraries/lithium/tests/cases/template/view/adapter/SimpleTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\template\view\adapter;
-use \lithium\template\view\adapter\Simple;
-use \lithium\tests\mocks\util\MockStringObject;
+use lithium\template\view\adapter\Simple;
+use lithium\tests\mocks\util\MockStringObject;
class SimpleTest extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/test/ControllerTest.php b/libraries/lithium/tests/cases/test/ControllerTest.php
index fbf1fed..325059f 100644
--- a/libraries/lithium/tests/cases/test/ControllerTest.php
+++ b/libraries/lithium/tests/cases/test/ControllerTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\test;
-use \lithium\test\Controller;
+use lithium\test\Controller;
class ControllerTest extends \lithium\test\Unit {
diff --git a/libraries/lithium/tests/cases/test/DispatcherTest.php b/libraries/lithium/tests/cases/test/DispatcherTest.php
index b83d93b..58ad03f 100644
--- a/libraries/lithium/tests/cases/test/DispatcherTest.php
+++ b/libraries/lithium/tests/cases/test/DispatcherTest.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\test;
-use \lithium\test\Dispatcher;
-use \lithium\util\Collection;
+use lithium\test\Dispatcher;
+use lithium\util\Collection;
class DispatcherTest extends \lithium\test\Unit {
@@ -19,9 +19,6 @@ class DispatcherTest extends \lithium\test\Unit {
$result = $report->group;
$this->assertTrue(is_a($result, '\lithium\test\Group'));
-
- $result = $report->reporter;
- $this->assertTrue(is_a($result, '\lithium\test\reporter\Text'));
}
public function testRunWithReporter() {
@@ -30,9 +27,6 @@ class DispatcherTest extends \lithium\test\Unit {
$result = $report->group;
$this->assertTrue(is_a($result, '\lithium\test\Group'));
-
- $result = $report->reporter;
- $this->assertTrue(is_a($result, '\lithium\test\reporter\Html'));
}
public function testRunCaseWithString() {
@@ -59,17 +53,21 @@ class DispatcherTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$expected = new Collection(array(
- 'items' => array(new \lithium\tests\mocks\test\cases\MockTest())
+ 'data' => array(
+ new \lithium\tests\mocks\test\cases\MockSkipThrowsException(),
+ new \lithium\tests\mocks\test\cases\MockTest(),
+ new \lithium\tests\mocks\test\cases\MockTestErrorHandling()
+ )
));
$result = $report->group->tests();
$this->assertEqual($expected, $result);
$expected = 'testNothing';
- $result = $report->results['group'][0][0]['method'];
+ $result = $report->results['group'][1][0]['method'];
$this->assertEqual($expected, $result);
$expected = 'pass';
- $result = $report->results['group'][0][0]['result'];
+ $result = $report->results['group'][1][0]['result'];
$this->assertEqual($expected, $result);
}
}
diff --git a/libraries/lithium/tests/cases/test/GroupTest.php b/libraries/lithium/tests/cases/test/GroupTest.php
index e9f9612..58017bb 100644
--- a/libraries/lithium/tests/cases/test/GroupTest.php
+++ b/libraries/lithium/tests/cases/test/GroupTest.php
@@ -2,14 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\test;
-use \lithium\test\Group;
-use \lithium\util\Collection;
+use lithium\test\Group;
+use lithium\util\Collection;
+use lithium\core\Libraries;
class GroupTest extends \lithium\test\Unit {
@@ -22,38 +23,48 @@ class GroupTest extends \lithium\test\Unit {
}
public function testAddCaseThroughConstructor() {
- $items = (array) "\lithium\\tests\mocks\\test";
- $group = new Group(compact('items'));
+ $data = (array) "\lithium\\tests\mocks\\test";
+ $group = new Group(compact('data'));
$expected = new Collection(array(
- 'items' => array(new \lithium\tests\mocks\test\cases\MockTest())
+ 'data' => array(
+ new \lithium\tests\mocks\test\cases\MockSkipThrowsException(),
+ new \lithium\tests\mocks\test\cases\MockTest(),
+ new \lithium\tests\mocks\test\cases\MockTestErrorHandling()
+ )
));
$result = $group->tests();
$this->assertEqual($expected, $result);
}
- public function testAddByString(){
+ public function testAddEmpty() {
$group = new Group();
- $result = $group->add('g11n');
+ $group->add('');
+ $group->add('\\');
+ $group->add('foobar');
+ $this->assertFalse($group->items());
+ }
+
+ public function testAddByString() {
+ $group = new Group();
+ $result = $group->add('lithium\tests\cases\g11n');
$expected = array(
'lithium\tests\cases\g11n\CatalogTest',
'lithium\tests\cases\g11n\LocaleTest',
'lithium\tests\cases\g11n\MessageTest',
'lithium\tests\cases\g11n\catalog\AdapterTest',
- 'lithium\tests\cases\g11n\catalog\adapter\CldrTest',
'lithium\tests\cases\g11n\catalog\adapter\CodeTest',
'lithium\tests\cases\g11n\catalog\adapter\GettextTest',
'lithium\tests\cases\g11n\catalog\adapter\PhpTest',
);
$this->assertEqual($expected, $result);
- $result = $group->add('data\ModelTest');
+ $result = $group->add('lithium\tests\cases\data\ModelTest');
$expected = array(
'lithium\tests\cases\g11n\CatalogTest',
'lithium\tests\cases\g11n\LocaleTest',
'lithium\tests\cases\g11n\MessageTest',
'lithium\tests\cases\g11n\catalog\AdapterTest',
- 'lithium\tests\cases\g11n\catalog\adapter\CldrTest',
'lithium\tests\cases\g11n\catalog\adapter\CodeTest',
'lithium\tests\cases\g11n\catalog\adapter\GettextTest',
'lithium\tests\cases\g11n\catalog\adapter\PhpTest',
@@ -63,30 +74,24 @@ class GroupTest extends \lithium\test\Unit {
}
public function testAddByMixedThroughConstructor() {
- $expected = new Collection(array('items' => array(
- new \lithium\tests\cases\data\ModelTest(),
+ $group = new Group(array('data' => array(
+ 'lithium\tests\cases\data\ModelTest',
new \lithium\tests\cases\core\ObjectTest()
)));
-
- $group = new Group(array('items' => array(
- 'data\ModelTest',
+ $expected = new Collection(array('data' => array(
+ new \lithium\tests\cases\data\ModelTest(),
new \lithium\tests\cases\core\ObjectTest()
)));
- $this->assertEqual($expected, $group->tests());
-
- $group = new Group(array('items' => array(array(
- 'Data\ModelTest',
- new \lithium\tests\cases\core\ObjectTest()
- ))));
- $this->assertEqual($group->tests(), $expected);
+ $result = $group->tests();
+ $this->assertEqual($expected, $result);
}
public function testTests() {
$group = new Group();
- $result = $group->add('g11n\CatalogTest');
$expected = array(
'lithium\tests\cases\g11n\CatalogTest',
);
+ $result = $group->add('lithium\tests\cases\g11n\CatalogTest');
$this->assertEqual($expected, $result);
$results = $group->tests();
@@ -96,16 +101,14 @@ class GroupTest extends \lithium\test\Unit {
$this->assertTrue(is_a($results->current(), 'lithium\tests\cases\g11n\CatalogTest'));
}
- public function testTestsRun() {
+ public function testAddEmptyTestsRun() {
$group = new Group();
- $result = $group->add('\lithium\tests\mocks\test\MockUnitTest');
- $expected = array(
- '\lithium\tests\mocks\test\MockUnitTest',
- );
+ $result = $group->add('lithium\tests\mocks\test\MockUnitTest');
+ $expected = array('lithium\tests\mocks\test\MockUnitTest');
$this->assertEqual($expected, $result);
$results = $group->tests();
- $this->assertTrue(is_a($results, '\lithium\util\Collection'));
+ $this->assertTrue(is_a($results, 'lithium\util\Collection'));
$results = $group->tests();
$this->assertTrue(is_a($results->current(), 'lithium\tests\mocks\test\MockUnitTest'));
@@ -124,16 +127,97 @@ class GroupTest extends \lithium\test\Unit {
$result = $results[0][0]['class'];
$this->assertEqual($expected, $result);
- $expected = realpath(str_replace('\\', '/', LITHIUM_LIBRARY_PATH)
- . '/lithium/tests/mocks/test/MockUnitTest.php');
+ $expected = str_replace('\\', '/', LITHIUM_LIBRARY_PATH);
+ $expected = realpath($expected . '/lithium/tests/mocks/test/MockUnitTest.php');
$result = $results[0][0]['file'];
$this->assertEqual($expected, str_replace('\\', '/', $result));
}
- public function testQueryAllTests() {
+ public function testGroupAllForLithium() {
+ Libraries::cache(false);
$result = Group::all(array('library' => 'lithium'));
$this->assertTrue(count($result) >= 60);
}
+
+ public function testAddTestAppGroup() {
+ $test_app = Libraries::get(true, 'resources') . '/tmp/tests/test_app';
+ mkdir($test_app);
+ Libraries::add('test_app', array('path' => $test_app));
+
+ mkdir($test_app . '/tests/cases/models', 0777, true);
+ file_put_contents($test_app . '/tests/cases/models/UserTest.php',
+ "<?php namespace test_app\\tests\\cases\\models;\n
+ class UserTest extends \\lithium\\test\\Unit { public function testMe() {
+ \$this->assertTrue(true);
+ }}"
+ );
+ Libraries::cache(false);
+
+ $expected = (array) Libraries::find('test_app', array(
+ 'recursive' => true,
+ 'path' => '/tests',
+ 'filter' => '/cases|integration|functional/',
+ ));
+
+ Libraries::cache(false);
+
+ $group = new Group();
+ $result = $group->add('test_app');
+ $this->assertEqual($expected, $result);
+
+ Libraries::cache(false);
+ $this->_cleanUp();
+ }
+
+ public function testRunGroupAllForTestApp() {
+ $test_app = Libraries::get(true, 'resources') . '/tmp/tests/test_app';
+ mkdir($test_app);
+ Libraries::add('test_app', array('path' => $test_app));
+
+ mkdir($test_app . '/tests/cases/models', 0777, true);
+ file_put_contents($test_app . '/tests/cases/models/UserTest.php',
+ "<?php namespace test_app\\tests\\cases\\models;\n
+ class UserTest extends \\lithium\\test\\Unit { public function testMe() {
+ \$this->assertTrue(true);
+ }}"
+ );
+ Libraries::cache(false);
+
+ $expected = array('test_app\\tests\\cases\\models\\UserTest');
+ $result = Group::all(array('library' => 'test_app'));
+ $this->assertEqual($expected, $result);
+
+ Libraries::cache(false);
+ $this->_cleanUp();
+ }
+
+ public function testRunGroupForTestAppModel() {
+ $test_app = Libraries::get(true, 'resources') . '/tmp/tests/test_app';
+ mkdir($test_app);
+ Libraries::add('test_app', array('path' => $test_app));
+
+ mkdir($test_app . '/tests/cases/models', 0777, true);
+ file_put_contents($test_app . '/tests/cases/models/UserTest.php',
+ "<?php namespace test_app\\tests\\cases\\models;\n
+ class UserTest extends \\lithium\\test\\Unit { public function testMe() {
+ \$this->assertTrue(true);
+ }}"
+ );
+ Libraries::cache(false);
+
+ $group = new Group(array('data' => array('\\test_app\\tests\\cases')));
+
+ $expected = array('test_app\\tests\\cases\\models\\UserTest');
+ $result = $group->to('array');
+ $this->assertEqual($expected, $result);
+
+ $expected = 'pass';
+ $result = $group->tests()->run();
+ $this->assertEqual($expected, $result[0][0]['result']);
+
+ Libraries::cache(false);
+ $this->_cleanUp();
+ }
}
-?>
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/IntegrationTest.php b/libraries/lithium/tests/cases/test/IntegrationTest.php
new file mode 100644
index 0000000..bcbb03f
--- /dev/null
+++ b/libraries/lithium/tests/cases/test/IntegrationTest.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\test;
+
+use lithium\tests\mocks\test\MockIntegrationTest;
+
+class IntegrationTest extends \lithium\test\Unit {
+
+ public function testIntegrationHaltsOnFail() {
+ $test = new MockIntegrationTest();
+
+ $expected = 2;
+ $report = $test->run();
+ $result = count($report);
+
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/ReportTest.php b/libraries/lithium/tests/cases/test/ReportTest.php
index 2499f52..2f501e1 100644
--- a/libraries/lithium/tests/cases/test/ReportTest.php
+++ b/libraries/lithium/tests/cases/test/ReportTest.php
@@ -2,21 +2,21 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\test;
-use \lithium\test\Report;
-use \lithium\test\Group;
+use lithium\test\Report;
+use lithium\test\Group;
class ReportTest extends \lithium\test\Unit {
public function testInit() {
$report = new Report(array(
'title' => '\lithium\tests\mocks\test\MockUnitTest',
- 'group' => new Group(array('items' => array('\lithium\tests\mocks\test\MockUnitTest')))
+ 'group' => new Group(array('data' => array('\lithium\tests\mocks\test\MockUnitTest')))
));
$report->run();
@@ -33,16 +33,64 @@ class ReportTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
+ public function testFilters() {
+ $report = new Report(array(
+ 'title' => '\lithium\tests\mocks\test\MockFilterClassTest',
+ 'group' => new Group(
+ array('data' => array('\lithium\tests\mocks\test\MockFilterClassTest'))
+ ),
+ 'filters' => array("Complexity" => ""),
+ 'format' => 'html',
+ 'reporter' => 'html'
+ ));
+
+ $expected = array('lithium\test\filter\Complexity' => array(
+ 'name' => 'complexity', 'apply' => array(), 'analyze' => array()
+ ));
+ $result = $report->filters();
+ $this->assertEqual($expected, $result);
+ }
+
public function testStats() {
$report = new Report(array(
'title' => '\lithium\tests\mocks\test\MockUnitTest',
- 'group' => new Group(array('items' => array('\lithium\tests\mocks\test\MockUnitTest')))
+ 'group' => new Group(array('data' => array('\lithium\tests\mocks\test\MockUnitTest')))
));
$report->run();
- $expected = "1 / 1 passes\n0 fails and 0 exceptions";
+ $expected = 1;
$result = $report->stats();
- $this->assertEqual($expected, $result);
+ $this->assertEqual($expected, $result['count']['asserts']);
+ $this->assertEqual($expected, $result['count']['passes']);
+ $this->assertTrue($result['success']);
+ }
+
+ public function testRender() {
+ $report = new Report(array(
+ 'title' => '\lithium\tests\mocks\test\MockUnitTest',
+ 'group' => new Group(array('data' => array('\lithium\tests\mocks\test\MockUnitTest'))),
+ 'format' => 'html',
+ 'reporter' => 'html'
+ ));
+ $report->run();
+
+ $result = $report->render("stats");
+ $this->assertPattern("/1 \/ 1 passes, 0 fails and 0 exceptions/", $result);
+ }
+
+ public function testSingleFilter() {
+ $report = new Report(array(
+ 'title' => '\lithium\tests\mocks\test\MockFilterClassTest',
+ 'group' => new Group(array(
+ 'data' => array('\lithium\tests\mocks\test\MockFilterClassTest')
+ )),
+ 'filters' => array("Complexity" => "")
+ ));
+ $report->run();
+
+ $class = 'lithium\test\filter\Complexity';
+ $result = $report->results['filters'][$class];
+ $this->assertTrue(isset($report->results['filters'][$class]));
}
}
diff --git a/libraries/lithium/tests/cases/test/ReporterTest.php b/libraries/lithium/tests/cases/test/ReporterTest.php
deleted file mode 100644
index 784a230..0000000
--- a/libraries/lithium/tests/cases/test/ReporterTest.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\test;
-
-use \lithium\test\Reporter;
-
-class ReporterTest extends \lithium\test\Unit {
-
- public function setUp() {
- $this->reporter = new Reporter();
- }
-
- public function testMenu() {
- $tests = array('lithium\tests\cases\test\reporter\HtmlTest');
- $expected = "";
- $result = $this->reporter->menu($tests);
- $this->assertEqual($expected, $result);
- }
-
- public function testMenuTree() {
- $tests = array('lithium\tests\cases\test\reporter\HtmlTest');
- $expected = "";
- $result = $this->reporter->menu($tests, array('tree' => true));
- $this->assertEqual($expected, $result);
- }
-
- public function testStats() {
- $stats = array(
- 'asserts' => 1,
- 'passes' => array(array(
- 'line' => 23, 'method' => 'testNothing',
- 'assertion' => 'assertEqual', 'message' => 'the message',
- 'class' => 'lithium\tests\cases\test\reporter\BaseTest'
- )),
- 'fails' => array(array('method' => 'testNothing')),
- 'errors' => array(),
- 'exceptions' => array(),
- );
- $expected = "";
- $result = $this->reporter->stats($stats);
- $this->assertEqual($expected, $result);
- }
-
- public function testStatsWithError() {
- $stats = array(
- 'asserts' => 1,
- 'passes' => array(array(
- 'line' => 23, 'method' => 'testNothing',
- 'assertion' => 'assertEqual', 'message' => 'the message',
- 'class' => 'lithium\tests\cases\test\reporter\BaseTest'
- )),
- 'fails' => array(array('method' => 'testNothing')),
- 'errors' => array(array('result' => 'fail')),
- 'exceptions' => array(),
- );
- $expected = "";
- $result = $this->reporter->stats($stats);
- $this->assertEqual($expected, $result);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/UnitTest.php b/libraries/lithium/tests/cases/test/UnitTest.php
index 90fc5b2..8c76dd6 100644
--- a/libraries/lithium/tests/cases/test/UnitTest.php
+++ b/libraries/lithium/tests/cases/test/UnitTest.php
@@ -2,12 +2,18 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\test;
+use Exception;
+use lithium\core\Libraries;
+use lithium\tests\mocks\test\MockUnitTest;
+use lithium\tests\mocks\test\cases\MockSkipThrowsException;
+use lithium\tests\mocks\test\cases\MockTestErrorHandling;
+
class UnitTest extends \lithium\test\Unit {
public function compare($type, $expected, $result = null) {
@@ -16,13 +22,29 @@ class UnitTest extends \lithium\test\Unit {
public function testBaseAssertions() {
$this->assert(true);
- //$this->assert(false);
+ $this->assert(false);
+ $result = array_pop($this->_results);
+ $this->assertEqual('fail', $result['result']);
$this->assertTrue(true);
$this->assertFalse(false);
}
- public function testCompare() {
- $expected = array('trace' => null, 'expected' => 'array', 'result' => 'string');
+ public function testCompareIsEqual() {
+ $result = $this->compare('equal', 'string', 'string');
+ $this->assertTrue($result);
+ }
+
+ public function testCompareIsIdentical() {
+ $result = $this->compare('identical', 'string', 'string');
+ $this->assertTrue($result);
+ }
+
+ public function testCompareTypes() {
+ $expected = array(
+ 'trace' => null,
+ 'expected' => "(array) Array\n(\n)",
+ 'result' => "(string) string"
+ );
$result = $this->compare('equal', array(), 'string');
$this->assertEqual($expected, $result);
}
@@ -34,9 +56,24 @@ class UnitTest extends \lithium\test\Unit {
}
public function testAssertEqualNumericFail() {
- $result = array(1, 2);
$expected = array(1, 2, 3);
- //$this->assertEqual($expected, $result);
+ $result = array(1, 2);
+ $this->assertEqual($expected, $result);
+
+ $expected = array(
+ 'result' => 'fail', 'file' => __FILE__, 'line' => __LINE__ - 3,
+ 'method' => 'testAssertEqualNumericFail', 'assertion' => 'assertEqual',
+ 'class' => __CLASS__, 'message' =>
+ "trace: [2]\nexpected: 3\n"
+ . "result: NULL\n",
+ 'data' => array(
+ 'trace' => '[2]',
+ 'expected' => 3,
+ 'result' => null
+ )
+ );
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result);
}
public function testAssertEqualAssociativeArray() {
@@ -51,41 +88,282 @@ class UnitTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
}
- /**
- * undocumented function
- *
- * @return void
- * @todo See @todo above.
- */
public function testAssertEqualThreeDFail() {
+ $expected = array(
+ array(array(1, 2), array(1, 2)),
+ array(array(1, 2), array(1, 2))
+ );
$result = array(
array(array(1, 2), array(1)),
array(array(1, 2), array(1))
);
+ $this->assertEqual($expected, $result);
+
$expected = array(
- array(array(1, 2), array(1, 2)),
- array(array(1, 2), array(1, 2))
+ 'result' => 'fail', 'file' => __FILE__, 'line' => __LINE__ - 3,
+ 'method' => 'testAssertEqualThreeDFail', 'assertion' => 'assertEqual',
+ 'class' => __CLASS__, 'message' =>
+ "trace: [0][1][1]\nexpected: 2\n"
+ . "result: NULL\n"
+ . "trace: [1][1][1]\nexpected: 2\n"
+ . "result: NULL\n",
+ 'data' => array(
+ array(
+ array(
+ 'trace' => '[0][1][1]',
+ 'expected' => 2,
+ 'result' => null
+ ),
+ ),
+ array(
+ array('trace' => '[1][1][1]',
+ 'expected' => 2,
+ 'result' => null
+ )
+ )
+ )
);
- //$this->assertEqual($expected, $result);
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result);
}
- public function testAssertIdentical() {
+ public function testAssertWithCustomMessage() {
+ $expected = false;
+ $result = true;
+ $this->assertEqual($expected, $result, 'Custom Message Test');
+ $expected = 'Custom Message Test';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['message']);
}
- public function testTestMethods() {
+ public function testSubject() {
+ $test = new MockUnitTest();
+ $expected = 'lithium\\tests\\mocks\\test\\MockUnit';
+ $result = $test->subject();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testRun() {
+ $test = new MockUnitTest();
$expected = array(
- 'testBaseAssertions', 'testCompare', 'testAssertEqualNumeric',
- 'testAssertEqualNumericFail', 'testAssertEqualAssociativeArray',
- 'testAssertEqualThreeDFail', 'testAssertIdentical', 'testTestMethods',
- 'testCleanUp', 'testCleanUpWithFullPath', 'testCleanUpWithRelativePath',
- 'testSkipIf'
+ 'result' => 'pass',
+ 'file' => realpath(LITHIUM_LIBRARY_PATH) . '/lithium/tests/mocks/test/MockUnitTest.php',
+ 'line' => 14,
+ 'method' => 'testNothing',
+ 'assertion' => 'assertTrue',
+ 'class' => 'lithium\\tests\\mocks\\test\\MockUnitTest',
+ 'message' => "expected: true\nresult: true\n",
+ 'data' => array('expected' => true, 'result' => true)
);
- $this->assertEqual($expected, $this->methods());
+ $result = $test->run();
+ $this->assertEqual($expected, $result[0]);
+ }
+
+ public function testAssertNotEqual() {
+ $expected = true;
+ $result = true;
+ $this->assertNotEqual($expected, $result);
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertNotEqual';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertNotEqual';
+ $this->assertEqual($expected, $result['assertion']);
+ }
+
+ public function testAssertIdentical() {
+ $expected = true;
+ $result = 1;
+ $this->assertIdentical($expected, $result);
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertIdentical';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertIdentical';
+ $this->assertEqual($expected, $result['assertion']);
+ }
+
+ public function testAssertIdenticalArray() {
+ $expected = array('1', '2', '3');
+ $result = array('1', '3', '4');
+ $this->assertIdentical($expected, $result);
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertIdenticalArray';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = "trace: [1]\nexpected: '2'\nresult: '3'\n";
+ $this->assertEqual($expected, $result['message']);
+ }
+
+ public function testAssertNull() {
+ $expected = null;
+ $result = null;
+ $this->assertNull($expected, $result);
+
+ $expected = 'pass';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertNull';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertNull';
+ $this->assertEqual($expected, $result['assertion']);
+ }
+
+ public function testAssertNoPattern() {
+ $expected = '/\s/';
+ $result = null;
+ $this->assertNoPattern($expected, $result);
+
+ $expected = 'pass';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertNoPattern';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertNoPattern';
+ $this->assertEqual($expected, $result['assertion']);
+ }
+
+ public function testAssertPattern() {
+ $expected = '/\s/';
+ $result = ' ';
+ $this->assertPattern($expected, $result);
+
+ $expected = 'pass';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertPattern';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertPattern';
+ $this->assertEqual($expected, $result['assertion']);
+ }
+
+ public function testAssertTags() {
+ $result = '<input id="test">';
+ $this->assertTags($result, array(
+ 'input' => array('id' => 'test')
+ ));
+
+ $expected = 'pass';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertTags';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertTags';
+ $this->assertEqual($expected, $result['assertion']);
+ }
+
+ public function testAssertTagsNoClosingTag() {
+ $result = '<span id="test">';
+ $this->assertTags($result, array(
+ 'span' => array('id' => 'test'), '/span'
+ ));
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertTagsNoClosingTag';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertTags';
+ $this->assertEqual($expected, $result['assertion']);
+
+ $expected = '- Item #2 / regex #3 failed: Close span tag';
+ $this->assertEqual($expected, $result['message']);
+ }
+
+ public function testAssertTagsMissingAttribute() {
+ $result = '<span></span>';
+ $this->assertTags($result, array(
+ 'span' => array('id' => 'test'), '/span'
+ ));
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertTagsMissingAttribute';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertTags';
+ $this->assertEqual($expected, $result['assertion']);
+
+ $expected = '- Item #1 / regex #1 failed: Attribute "id" == "test"';
+ $this->assertEqual($expected, $result['message']);
+ }
+
+ public function testAssertTagsString() {
+ $result = '<span>ok</span>';
+ $this->assertTags($result, array('<span'));
+
+ $expected = 'pass';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertTagsString';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertTags';
+ $this->assertEqual($expected, $result['assertion']);
+ }
+
+ public function testAssertTagsFailTextEqual() {
+ $result = '<span>ok</span>';
+ $this->assertTags($result, array('span'));
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testAssertTagsFailTextEqual';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = '- Item #1 / regex #0 failed: Text equals "span"';
+ $this->assertEqual($expected, $result['message']);
+ }
+
+ public function testIdenticalArrayFail() {
+ $expected = array('1', '2', '3');
+ $result = array(1, '2', '3');;
+ $this->assertIdentical($expected, $result);
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = 'testIdenticalArrayFail';
+ $this->assertEqual($expected, $result['method']);
+
+ $expected = 'assertIdentical';
+ $this->assertEqual($expected, $result['assertion']);
+
+ $expected = "trace: [0]\nexpected: '(string) 1'\nresult: '(integer) 1'\n";
+ $this->assertEqual($expected, $result['message']);
}
public function testCleanUp() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
$this->assertTrue(mkdir("{$base}/cleanup_test"));
@@ -97,7 +375,7 @@ class UnitTest extends \lithium\test\Unit {
}
public function testCleanUpWithFullPath() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
$this->assertTrue(mkdir("{$base}/cleanup_test"));
@@ -113,7 +391,7 @@ class UnitTest extends \lithium\test\Unit {
}
public function testCleanUpWithRelativePath() {
- $base = LITHIUM_APP_PATH . '/resources/tmp/tests';
+ $base = Libraries::get(true, 'resources') . '/tmp/tests';
$this->skipIf(!is_writable($base), "{$base} is not writable.");
$this->assertTrue(mkdir("{$base}/cleanup_test"));
@@ -131,12 +409,181 @@ class UnitTest extends \lithium\test\Unit {
public function testSkipIf() {
try {
$this->skipIf(true, 'skip me');
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$result = $e->getMessage();
}
$expected = 'skip me';
$this->assertEqual($expected, $result);
}
+
+ public function testExpectException() {
+ $this->expectException('test expected exception');
+ $expected = 'test expected exception';
+ $result = array_pop($this->_expected);
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHandleException() {
+ $this->_handleException(new Exception('test handle exception'));
+ $expected = 'test handle exception';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['message']);
+ }
+
+ public function testExpectExceptionRegex() {
+ $this->expectException('/test handle exception/');
+ $this->_handleException(new Exception('test handle exception'));
+ $expected = 'test handle exception';
+ $this->assertTrue(empty($this->_expected));
+ }
+
+ public function testGetTest() {
+ $test = static::get('lithium\test\Unit');
+ $this->assertEqual($test, __CLASS__);
+ }
+
+ public function testAssertCookie() {
+ $expected = array(
+ 'key' => 'key2.nested', 'value' => 'value1', 'expires' => 'May 04 2010 14:02:36 EST'
+ );
+ $this->assertCookie($expected);
+
+ $expected = 'fail';
+ $result = array_pop($this->_results);
+ $this->assertEqual($expected, $result['result']);
+
+ $expected = '/not found in headers./';
+ $this->assertPattern($expected, $result['message']);
+ }
+
+ public function testAssertCookieWithHeaders() {
+ $headers = array(
+ 'Set-Cookie: name[key]=value; expires=Tue, 04-May-2010 19:02:36 GMT; path=/',
+ 'Set-Cookie: name[key1]=value1; expires=Tue, 04-May-2010 19:02:36 GMT; path=/',
+ 'Set-Cookie: name[key2][nested]=value1; expires=Tue, 04-May-2010 19:02:36 GMT; path=/'
+ );
+
+ $this->assertCookie(array('key' => 'key', 'value' => 'value'), $headers);
+ $this->assertCookie(array('key' => 'key1', 'value' => 'value1'), $headers);
+ $this->assertCookie(array('key' => 'key2.nested', 'value' => 'value1'), $headers);
+
+ $expected = array(
+ 'key' => 'key2.nested', 'value' => 'value1', 'expires' => 'May 04 2010 14:02:36 EST'
+ );
+ $this->assertCookie($expected, $headers);
+ }
+
+ public function testCompareWithEmptyResult() {
+ $result = $this->compare('equal', array('key' => array('val1', 'val2')), array());
+ $expected = array(
+ 'trace' => '[key]',
+ 'expected' => array('val1', 'val2'),
+ 'result' => array()
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExceptionCatching() {
+ $test = new MockSkipThrowsException();
+ $test->run();
+ $expected = 'skip throws exception';
+ $results = $test->results();
+ $this->assertEqual($expected, $results[0]['message']);
+ }
+
+ public function testErrorHandling() {
+ $test = new MockTestErrorHandling();
+ $test->run();
+ $expected = '/Missing argument 1/';
+ $results = $test->results();
+ $this->assertPattern($expected, $results[0]['message']);
+
+ $expected = '/Unit::_arrayPermute()/';
+ $this->assertPattern($expected, $results[0]['message']);
+ }
+
+ public function testAssertObjects() {
+ $expected = (object) array('one' => 'two');
+ $result = (object) array('one' => 'not-two');
+ $this->assertEqual($expected, $result);
+
+ $result = array_pop($this->_results);
+ $expected = "one";
+ $this->assertEqual($expected, $result['data']['trace']);
+ }
+
+ public function testAssertArrayIdentical() {
+ $expected = array('one' => array('one'));
+ $result = array('one' => array());
+ $this->assertIdentical($expected, $result);
+
+ $result = array_pop($this->_results);
+ $expected = "[one]";
+ $this->assertEqual($expected, $result['data']['trace']);
+ }
+
+ public function testCompareIdenticalArray() {
+ $expected = array(
+ 'trace' => null,
+ 'expected' => array(),
+ 'result' => array('two', 'values')
+ );
+ $result = $this->compare('identical', array(), array('two', 'values'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function imethods() {
+ return array('testCompareIdenticalArray');
+ }
+
+ public function testCompareEqualNullArray() {
+ $expected = array('trace' => null, 'expected' => array(), 'result' => array(null));
+ $result = $this->compare('equal', array(), array(null));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCompareIdenticalNullArray() {
+ $expected = array('trace' => null, 'expected' => array(), 'result' => array(null));
+ $result = $this->compare('identical', array(), array(null));
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Always keep second to last.
+ *
+ */
+ public function testResults() {
+ $expected = 89;
+ $result = count($this->results());
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Always keep last.
+ *
+ */
+ public function testTestMethods() {
+ $expected = array(
+ 'testBaseAssertions', 'testCompareIsEqual', 'testCompareIsIdentical',
+ 'testCompareTypes', 'testAssertEqualNumeric',
+ 'testAssertEqualNumericFail', 'testAssertEqualAssociativeArray',
+ 'testAssertEqualThreeDFail', 'testAssertWithCustomMessage',
+ 'testSubject', 'testRun', 'testAssertNotEqual', 'testAssertIdentical',
+ 'testAssertIdenticalArray',
+ 'testAssertNull', 'testAssertNoPattern', 'testAssertPattern', 'testAssertTags',
+ 'testAssertTagsNoClosingTag', 'testAssertTagsMissingAttribute',
+ 'testAssertTagsString', 'testAssertTagsFailTextEqual', 'testIdenticalArrayFail',
+ 'testCleanUp', 'testCleanUpWithFullPath', 'testCleanUpWithRelativePath',
+ 'testSkipIf', 'testExpectException', 'testHandleException', 'testExpectExceptionRegex',
+ 'testGetTest', 'testAssertCookie', 'testAssertCookieWithHeaders',
+ 'testCompareWithEmptyResult',
+ 'testExceptionCatching', 'testErrorHandling', 'testAssertObjects',
+ 'testAssertArrayIdentical', 'testCompareIdenticalArray',
+ 'testCompareEqualNullArray', 'testCompareIdenticalNullArray',
+ 'testResults', 'testTestMethods'
+ );
+ $this->assertIdentical($expected, $this->methods());
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/filter/AffectedTest.php b/libraries/lithium/tests/cases/test/filter/AffectedTest.php
index 71b12c0..4c53dc5 100644
--- a/libraries/lithium/tests/cases/test/filter/AffectedTest.php
+++ b/libraries/lithium/tests/cases/test/filter/AffectedTest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -21,13 +21,14 @@ class AffectedTest extends \lithium\test\Unit {
public function testSingleTest() {
$group = new Group();
- $group->add('\lithium\tests\cases\g11n\CatalogTest');
+ $group->add('lithium\tests\cases\g11n\CatalogTest');
$this->report->group = $group;
- $tests = Affected::apply($this->report);
+ $tests = Affected::apply($this->report, $group->tests());
$expected = array(
'lithium\tests\cases\g11n\CatalogTest',
- 'lithium\tests\cases\g11n\MessageTest'
+ 'lithium\tests\cases\g11n\MessageTest',
+ 'lithium\tests\cases\console\command\g11n\ExtractTest'
);
$result = $tests->map('get_class', array('collect' => false));
$this->assertEqual($expected, $result);
@@ -35,28 +36,27 @@ class AffectedTest extends \lithium\test\Unit {
public function testSingleTestWithSingleResult() {
$group = new Group();
- $group->add('\lithium\tests\cases\core\StaticObjectTest');
+ $group->add('lithium\tests\cases\core\StaticObjectTest');
$this->report->group = $group;
- $tests = Affected::apply($this->report);
+ $tests = Affected::apply($this->report, $group->tests());
- $expected = array(
- 'lithium\tests\cases\core\StaticObjectTest'
- );
+ $expected = array('lithium\tests\cases\core\StaticObjectTest');
$result = $tests->map('get_class', array('collect' => false));
$this->assertEqual($expected, $result);
}
public function testMultipleTests() {
$group = new Group();
- $group->add('\lithium\tests\cases\g11n\CatalogTest');
- $group->add('\lithium\tests\cases\analysis\LoggerTest');
+ $group->add('lithium\tests\cases\g11n\CatalogTest');
+ $group->add('lithium\tests\cases\analysis\LoggerTest');
$this->report->group = $group;
- $tests = Affected::apply($this->report);
+ $tests = Affected::apply($this->report, $group->tests());
$expected = array(
'lithium\tests\cases\g11n\CatalogTest',
'lithium\tests\cases\analysis\LoggerTest',
- 'lithium\tests\cases\g11n\MessageTest'
+ 'lithium\tests\cases\g11n\MessageTest',
+ 'lithium\tests\cases\console\command\g11n\ExtractTest'
);
$result = $tests->map('get_class', array('collect' => false));
$this->assertEqual($expected, $result);
@@ -64,25 +64,35 @@ class AffectedTest extends \lithium\test\Unit {
public function testCyclicDependency() {
$group = new Group();
- $group->add('\lithium\tests\cases\g11n\CatalogTest');
- $group->add('\lithium\tests\cases\g11n\MessageTest');
+ $group->add('lithium\tests\cases\g11n\CatalogTest');
+ $group->add('lithium\tests\cases\g11n\MessageTest');
$this->report->group = $group;
- $tests = Affected::apply($this->report);
+ $tests = Affected::apply($this->report, $group->tests());
$expected = array(
'lithium\tests\cases\g11n\CatalogTest',
- 'lithium\tests\cases\g11n\MessageTest'
+ 'lithium\tests\cases\g11n\MessageTest',
+ 'lithium\tests\cases\console\command\g11n\ExtractTest'
);
$result = $tests->map('get_class', array('collect' => false));
$this->assertEqual($expected, $result);
}
- public function testOutputWithEmptyAnalysis() {
- $expected = "Additional Affected Tests\n-------------------------";
+ public function testAnalyze() {
+ $ns = 'lithium\tests\cases';
+
+ $expected = array(
+ 'lithium\g11n\Message' => "{$ns}\g11n\MessageTest",
+ 'lithium\console\command\g11n\Extract' => "{$ns}\console\command\g11n\ExtractTest"
+ );
- $result = Affected::output('text', array());
+ $group = new Group();
+ $group->add('lithium\tests\cases\g11n\CatalogTest');
+ $this->report->group = $group;
+ $tests = Affected::apply($this->report, $group->tests());
+ $results = Affected::analyze($this->report);
- $this->assertEqual($expected, $result);
+ $this->assertEqual($results, $expected);
}
}
diff --git a/libraries/lithium/tests/cases/test/reporter/HtmlTest.php b/libraries/lithium/tests/cases/test/reporter/HtmlTest.php
deleted file mode 100644
index 52a4155..0000000
--- a/libraries/lithium/tests/cases/test/reporter/HtmlTest.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\test\reporter;
-
-use \lithium\net\http\Router;
-use \lithium\action\Request;
-use \lithium\test\reporter\Html;
-use \lithium\tests\mocks\test\reporter\MockHtml;
-
-class HtmlTest extends \lithium\test\Unit {
-
- public function setUp() {
- $this->html = new Html();
- $this->mock = new MockHtml();
- $this->_routes = Router::get();
- Router::reset();
- Router::connect('/test/{:args}', array('controller' => '\lithium\test\Controller'));
- Router::connect('/test', array('controller' => '\lithium\test\Controller'));
- $this->request = new Request(array(
- 'base' => null,
- 'env' => array('PHP_SELF' => '/', 'DOCUMENT_ROOT' => '/')
- ));
- }
-
- public function tearDown() {
- Router::reset();
-
- foreach ($this->_routes as $route) {
- Router::connect($route);
- }
- }
-
- public function testMenuWithoutData() {
- $expected = '<ul></ul>';
- $result = $this->html->menu(array());
- $this->assertEqual($expected, $result);
- }
-
- public function testFormatGroup() {
- $expected = '<ul><li><a href="/test/lithium/tests">lithium</a>';
- $expected .= '<ul><li><a href="/test/lithium/tests/cases">cases</a>';
- $expected .= '<ul><li><a href="/test/lithium/tests/cases/core">core</a>';
- $expected .= '<ul><li><a href="/test/lithium/tests/cases/core/LibrariesTest">'
- . 'LibrariesTest</a></li>';
- $expected .= '</ul></li></ul></li></ul></li></ul>';
-
- $result = $this->html->menu(array('lithium\tests\cases\core\LibrariesTest'), array(
- 'tree' => true, 'request' => $this->request
- ));
- $this->assertEqual($expected, $result);
- }
-
- public function testFormatCase() {
- $tests = array('lithium\tests\cases\test\reporter\HtmlTest');
- $expected = '<ul><li><a href="/test/lithium/tests/cases/test/reporter/HtmlTest">'
- . 'HtmlTest</a></li></ul>';
- $result = $this->html->menu($tests, array('request' => $this->request));
- $this->assertEqual($expected, $result);
- }
-
- public function testFormatCaseWithRequestParams() {
- $this->request->params = array('args' => array('lithium', 'tests', 'cases'));
-
- $tests = array('lithium\tests\cases\test\reporter\HtmlTest');
- $expected = '<ul><li><a href="/test/lithium/tests/cases/test/reporter/HtmlTest">'
- . 'HtmlTest</a></li></ul>';
- $result = $this->html->menu($tests, array('request' => $this->request));
- $this->assertEqual($expected, $result);
- }
-
- public function testResult() {
- $stats = array(
- 'success' => false,
- 'passes' => 1, 'asserts' => 2, 'fails' => 1, 'exceptions' => 0
- );
- $expected = "<div class=\"test-result test-result-fail\">";
- $expected .= "1 / 2 passes, 1 fail and 0 exceptions";
- $expected .= "</div>";
- $result = $this->mock->result($stats);
- $this->assertEqual($expected, $result);
- }
-
- public function testFail() {
- $fail = array(
- 'assertion' => 'assertEqual',
- 'class' => 'MockTest', 'method' => 'testNothing', 'line' => 8,
- 'message' => 'the message',
- );
- $expected = "<div class=\"test-assert test-assert-failed\">";
- $expected .= "Assertion 'assertEqual' failed in MockTest::testNothing() on line 8: ";
- $expected .= "<span class=\"content\">the message</span>";
- $expected .= "</div>";
- $result = $this->mock->fail($fail);
- $this->assertEqual($expected, $result);
- }
-
- public function testException() {
- $exception = array(
- 'class' => 'MockTest', 'method' => 'testNothing', 'line' => 8,
- 'message' => 'the message', 'trace' => 'the trace'
- );
-
- $expected = "<div class=\"test-exception\">";
- $expected .= "Exception thrown in MockTest::testNothing() on line 8: ";
- $expected .= "<span class=\"content\">the message</span>";
- $expected .= "Trace: <span class=\"trace\">the trace</span>";
- $expected .= "</div>";
-
- $result = $this->mock->exception($exception);
- $this->assertEqual($expected, $result);
- }
-
- public function testSkip() {
- $exception = array(
- 'trace' => array(array(), array(
- 'class' => 'MockTest', 'function' => 'testNothing', 'line' => 8
- )),
- 'message' => 'skip this test',
- );
- $expected = "<div class=\"test-skip\">";
- $expected .= "Skip MockTest::testNothing() on line 8: ";
- $expected .= "<span class=\"content\">skip this test</span>";
- $expected .= "</div>";
- $result = $this->mock->skip($exception);
- $this->assertEqual($expected, $result);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/reporter/TextTest.php b/libraries/lithium/tests/cases/test/reporter/TextTest.php
deleted file mode 100644
index 42d82db..0000000
--- a/libraries/lithium/tests/cases/test/reporter/TextTest.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\cases\test\reporter;
-
-use \lithium\test\reporter\Text;
-use \lithium\tests\mocks\test\reporter\MockText;
-
-class TextTest extends \lithium\test\Unit {
-
- public $text = null;
-
- public $mock = null;
-
- public function setUp() {
- $this->text = new Text();
- $this->mock = new MockText();
- }
-
- public function testMenu() {
- $tests = array('lithium\tests\cases\test\reporter\HtmlTest');
- $expected = "\n-case lithium.tests.cases.test.reporter.HtmlTest\n\n";
- $result = $this->text->menu($tests, array('format' => 'text'));
- $this->assertEqual($expected, $result);
- }
-
- public function testResult() {
- $stats = array('passes' => 1, 'asserts' => 2, 'fails' => 1, 'exceptions' => 0);
- $expected = "1 / 2 passes\n1 fail and 0 exceptions";
- $result = $this->mock->result($stats);
- $this->assertEqual($expected, $result);
- }
-
- public function testFail() {
- $fail = array(
- 'assertion' => 'assertEqual',
- 'class' => 'MockTest', 'method' => 'testNothing', 'line' => 8,
- 'message' => 'the message',
- );
- $expected = "Assertion `assertEqual` failed in `MockTest::testNothing()` on line 8: ";
- $expected .= "\nthe message";
- $result = $this->mock->fail($fail);
- $this->assertEqual($expected, $result);
- }
-
- public function testException() {
- $exception = array(
- 'class' => 'MockTest', 'method' => 'testNothing', 'line' => 8,
- 'message' => 'the message', 'trace' => 'the trace'
- );
- $expected = "Exception thrown in `MockTest::testNothing()` on line 8:\n";
- $expected .= "the message\nTrace: the trace";
- $result = $this->mock->exception($exception);
- $this->assertEqual($expected, $result);
- }
-
- public function testSkip() {
- $exception = array(
- 'trace' => array(array(), array(
- 'class' => 'MockTest', 'function' => 'testNothing', 'line' => 8
- )),
- 'message' => 'skip this test',
- );
- $expected = "Skip MockTest::testNothing() on line 8:\n";
- $expected .= "skip this test";
- $result = $this->mock->skip($exception);
- $this->assertEqual($expected, $result);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/CollectionTest.php b/libraries/lithium/tests/cases/util/CollectionTest.php
index 7986256..5c47ae6 100644
--- a/libraries/lithium/tests/cases/util/CollectionTest.php
+++ b/libraries/lithium/tests/cases/util/CollectionTest.php
@@ -2,17 +2,17 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\util;
-use \stdClass;
-use \lithium\util\Collection;
-use \lithium\tests\mocks\util\MockCollectionMarker;
-use \lithium\tests\mocks\util\MockCollectionObject;
-use \lithium\tests\mocks\util\MockCollectionStringCast;
+use stdClass;
+use lithium\util\Collection;
+use lithium\tests\mocks\util\MockCollectionMarker;
+use lithium\tests\mocks\util\MockCollectionObject;
+use lithium\tests\mocks\util\MockCollectionStringCast;
class CollectionTest extends \lithium\test\Unit {
@@ -26,7 +26,7 @@ class CollectionTest extends \lithium\test\Unit {
$this->assertEqual($collection[0], 'foo');
$this->assertEqual(count($collection), 1);
- $collection = new Collection(array('items' => array('foo')));
+ $collection = new Collection(array('data' => array('foo')));
$this->assertEqual($collection[0], 'foo');
$this->assertEqual(count($collection), 1);
}
@@ -46,7 +46,9 @@ class CollectionTest extends \lithium\test\Unit {
$result = $collection->invoke('mapArray', array(), array('merge' => true));
$this->assertEqual($result, array_fill(0, 10, 'foo'));
- $collection = new Collection(array('items' => array_fill(0, 10, new MockCollectionObject())));
+ $collection = new Collection(array(
+ 'data' => array_fill(0, 10, new MockCollectionObject())
+ ));
$result = $collection->testFoo();
$this->assertEqual($result, array_fill(0, 10, 'testFoo'));
@@ -56,17 +58,23 @@ class CollectionTest extends \lithium\test\Unit {
}
public function testObjectCasting() {
- $collection = new Collection(array('items' => array_fill(0, 10, new MockCollectionObject())));
+ $collection = new Collection(array(
+ 'data' => array_fill(0, 10, new MockCollectionObject())
+ ));
$result = $collection->to('array');
$expected = array_fill(0, 10, array(1 => 2, 2 => 3));
$this->assertEqual($expected, $result);
- $collection = new Collection(array('items' => array_fill(0, 10, new MockCollectionMarker())));
+ $collection = new Collection(array(
+ 'data' => array_fill(0, 10, new MockCollectionMarker())
+ ));
$result = $collection->to('array');
$expected = array_fill(0, 10, array('marker' => false, 'data' => 'foo'));
$this->assertEqual($expected, $result);
- $collection = new Collection(array('items' => array_fill(0, 10, new MockCollectionStringCast())));
+ $collection = new Collection(array(
+ 'data' => array_fill(0, 10, new MockCollectionStringCast())
+ ));
$result = $collection->to('array');
$expected = array_fill(0, 10, json_encode(array(1 => 2, 2 => 3)));
$this->assertEqual($expected, $result);
@@ -78,7 +86,7 @@ class CollectionTest extends \lithium\test\Unit {
* @return void
*/
public function testCollectionFindFilter() {
- $collection = new Collection(array('items' => array_merge(
+ $collection = new Collection(array('data' => array_merge(
array_fill(0, 10, 1),
array_fill(0, 10, 2)
)));
@@ -99,19 +107,19 @@ class CollectionTest extends \lithium\test\Unit {
* @return void
*/
public function testCollectionFirstFilter() {
- $collection = new Collection(array('items' => array(0, 1, 2)));
+ $collection = new Collection(array('data' => array(0, 1, 2)));
$result = $collection->first(function($value) { return $value; });
$this->assertEqual(1, $result);
- $collection = new Collection(array('items' => array('Hello', '', 'Goodbye')));
+ $collection = new Collection(array('data' => array('Hello', '', 'Goodbye')));
$result = $collection->first(function($value) { return $value; });
$this->assertEqual('Hello', $result);
- $collection = new Collection(array('items' => array('', 'Hello', 'Goodbye')));
+ $collection = new Collection(array('data' => array('', 'Hello', 'Goodbye')));
$result = $collection->first(function($value) { return $value; });
$this->assertEqual('Hello', $result);
- $collection = new Collection(array('items' => array('', 'Hello', 'Goodbye')));
+ $collection = new Collection(array('data' => array('', 'Hello', 'Goodbye')));
$result = $collection->first();
$this->assertEqual('', $result);
}
@@ -123,7 +131,7 @@ class CollectionTest extends \lithium\test\Unit {
* @return void
*/
public function testCollectionEachFilter() {
- $collection = new Collection(array('items' => array(1, 2, 3, 4, 5)));
+ $collection = new Collection(array('data' => array(1, 2, 3, 4, 5)));
$filter = function($item) { return ++$item; };
$result = $collection->each($filter);
@@ -132,7 +140,7 @@ class CollectionTest extends \lithium\test\Unit {
}
public function testCollectionMapFilter() {
- $collection = new Collection(array('items' => array(1, 2, 3, 4, 5)));
+ $collection = new Collection(array('data' => array(1, 2, 3, 4, 5)));
$filter = function($item) { return ++$item; };
$result = $collection->map($filter);
@@ -150,7 +158,7 @@ class CollectionTest extends \lithium\test\Unit {
* @return void
*/
public function testArrayAccessOffsetMethods() {
- $collection = new Collection(array('items' => array('foo', 'bar', 'baz' => 'dib')));
+ $collection = new Collection(array('data' => array('foo', 'bar', 'baz' => 'dib')));
$this->assertTrue($collection->offsetExists(0));
$this->assertTrue($collection->offsetExists(1));
$this->assertTrue($collection->offsetExists('0'));
@@ -173,7 +181,7 @@ class CollectionTest extends \lithium\test\Unit {
* @return void
*/
public function testArrayAccessTraversalMethods() {
- $collection = new Collection(array('items' => array('foo', 'bar', 'baz' => 'dib')));
+ $collection = new Collection(array('data' => array('foo', 'bar', 'baz' => 'dib')));
$this->assertEqual('foo', $collection->current());
$this->assertEqual('bar', $collection->next());
$this->assertEqual('foo', $collection->prev());
@@ -218,7 +226,7 @@ class CollectionTest extends \lithium\test\Unit {
* @return void
*/
public function testInternalKeys() {
- $collection = new Collection(array('items' => array('foo', 'bar', 'baz' => 'dib')));
+ $collection = new Collection(array('data' => array('foo', 'bar', 'baz' => 'dib')));
$this->assertEqual(array(0, 1, 'baz'), $collection->keys());
}
@@ -229,11 +237,11 @@ class CollectionTest extends \lithium\test\Unit {
* @return void
*/
public function testCollectionFormatConversion() {
- Collection::formats('\lithium\net\http\Media');
- $items = array('hello', 'goodbye', 'foo' => array('bar', 'baz' => 'dib'));
- $collection = new Collection(compact('items'));
+ Collection::formats('lithium\net\http\Media');
+ $data = array('hello', 'goodbye', 'foo' => array('bar', 'baz' => 'dib'));
+ $collection = new Collection(compact('data'));
- $expected = json_encode($items);
+ $expected = json_encode($data);
$result = $collection->to('json');
$this->assertEqual($expected, $result);
diff --git a/libraries/lithium/tests/cases/util/InflectorTest.php b/libraries/lithium/tests/cases/util/InflectorTest.php
index 7c0de5c..a5d4c84 100644
--- a/libraries/lithium/tests/cases/util/InflectorTest.php
+++ b/libraries/lithium/tests/cases/util/InflectorTest.php
@@ -2,16 +2,19 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\util;
-use \lithium\util\Inflector;
+use lithium\util\Inflector;
+use lithium\analysis\Inspector;
class InflectorTest extends \lithium\test\Unit {
+ public static $_test = 'bar';
+
public function tearDown() {
Inflector::reset();
}
@@ -21,7 +24,7 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testInflectingSingulars() {
+ public function testSingularize() {
$this->assertEqual(Inflector::singularize('categorias'), 'categoria');
$this->assertEqual(Inflector::singularize('menus'), 'menu');
$this->assertEqual(Inflector::singularize('news'), 'news');
@@ -67,7 +70,7 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testInflectingPlurals() {
+ public function testPluralize() {
$this->assertEqual(Inflector::pluralize('categoria'), 'categorias');
$this->assertEqual(Inflector::pluralize('house'), 'houses');
$this->assertEqual(Inflector::pluralize('powerhouse'), 'powerhouses');
@@ -116,7 +119,7 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testInflectorReplace() {
+ public function testSlug() {
$result = Inflector::slug('Foo Bar: Not just for breakfast any-more');
$expected = 'Foo-Bar-Not-just-for-breakfast-any-more';
$this->assertEqual($expected, $result);
@@ -161,6 +164,10 @@ class InflectorTest extends \lithium\test\Unit {
$result = Inflector::slug('#this melts your face1#2#3');
$expected = 'this-melts-your-face1-2-3';
$this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('ThisMeltsYourFace');
+ $expected = 'This-Melts-Your-Face';
+ $this->assertEqual($expected, $result);
}
public function testAddingInvalidRules() {
@@ -212,7 +219,7 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testIrregularSynchronicity() {
+ public function testIrregularWords() {
$expectedPlural = Inflector::rules('plural');
$this->assertFalse(isset($expectedPlural['irregular']['bar']));
@@ -239,8 +246,9 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testVariableNaming() {
- $this->assertEqual(Inflector::camelize('test_field', false), 'testField');
+ public function testCamelize() {
+ $this->assertEqual(Inflector::camelize('test-field'), 'TestField');
+ $this->assertEqual(Inflector::camelize('test_field'), 'TestField');
$this->assertEqual(Inflector::camelize('test_fieLd', false), 'testFieLd');
$this->assertEqual(Inflector::camelize('test field', false), 'testField');
$this->assertEqual(Inflector::camelize('Test_field', false), 'testField');
@@ -251,7 +259,7 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testClassNaming() {
+ public function testClassify() {
$this->assertEqual(Inflector::classify('artists_genres'), 'ArtistsGenre');
$this->assertEqual(Inflector::classify('file_systems'), 'FileSystem');
$this->assertEqual(Inflector::classify('news'), 'News');
@@ -262,7 +270,7 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testTableNaming() {
+ public function testTabelize() {
$this->assertEqual(Inflector::tableize('ArtistsGenre'), 'artists_genres');
$this->assertEqual(Inflector::tableize('FileSystem'), 'file_systems');
$this->assertEqual(Inflector::tableize('News'), 'news');
@@ -273,7 +281,7 @@ class InflectorTest extends \lithium\test\Unit {
*
* @return void
*/
- public function testHumanization() {
+ public function testHumanize() {
$this->assertEqual(Inflector::humanize('posts'), 'Posts');
$this->assertEqual(Inflector::humanize('posts_tags'), 'Posts Tags');
$this->assertEqual(Inflector::humanize('file_systems'), 'File Systems');
@@ -302,7 +310,6 @@ class InflectorTest extends \lithium\test\Unit {
$this->assertEqual(Inflector::slug('ABc'), 'ABc');
Inflector::rules('transliteration', array('AB' => 'a'));
- Inflector::reset();
$this->assertEqual(Inflector::slug('ABc'), 'aac');
}
@@ -311,6 +318,52 @@ class InflectorTest extends \lithium\test\Unit {
Inflector::rules('uninflected', 'bord');
$this->assertEqual(Inflector::pluralize('bord'), 'bord');
}
+
+ /**
+ * Tests the storage mechanism for `$_underscored`, `$_camelized`,
+ * `$_humanized` and `$_pluralized`.
+ *
+ * @return void
+ */
+ public function testStorageMechanism() {
+ Inflector::reset();
+
+ $expected = array('TestField' => 'test_field');
+ $this->assertFalse($this->getProtectedValue('$_underscored'));
+ $this->assertEqual(Inflector::underscore('TestField'), 'test_field');
+ $this->assertEqual($expected, $this->getProtectedValue('$_underscored'));
+ $this->assertEqual(Inflector::underscore('TestField'), 'test_field');
+
+ $expected = array('test_field' => 'TestField');
+ $this->assertFalse($this->getProtectedValue('$_camelized'));
+ $this->assertEqual(Inflector::camelize('test_field', true), 'TestField');
+ $this->assertEqual($expected, $this->getProtectedValue('$_camelized'));
+ $this->assertEqual(Inflector::camelize('test_field', true), 'TestField');
+
+ $expected = array('test_field:_' => 'Test Field');
+ $this->assertFalse($this->getProtectedValue('$_humanized'));
+ $this->assertEqual(Inflector::humanize('test_field'), 'Test Field');
+ $this->assertEqual($expected, $this->getProtectedValue('$_humanized'));
+ $this->assertEqual(Inflector::humanize('test_field'), 'Test Field');
+
+ $expected = array('field' => 'fields');
+ $this->assertFalse($this->getProtectedValue('$_pluralized'));
+ $this->assertEqual(Inflector::pluralize('field'), 'fields');
+ $this->assertEqual($expected, $this->getProtectedValue('$_pluralized'));
+ $this->assertEqual(Inflector::pluralize('field'), 'fields');
+ }
+
+ /**
+ * This is a helper method for testStorageMechanism to fetch a private
+ * property of the Inflector class.
+ *
+ * @param string $property
+ * @return string The value of the property.
+ */
+ private function getProtectedValue($property) {
+ $info = Inspector::info("lithium\util\Inflector::{$property}");
+ return $info['value'];
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/SetTest.php b/libraries/lithium/tests/cases/util/SetTest.php
index 92ad8bd..b3ca762 100644
--- a/libraries/lithium/tests/cases/util/SetTest.php
+++ b/libraries/lithium/tests/cases/util/SetTest.php
@@ -2,44 +2,45 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\util;
-use \lithium\util\Set;
+use stdClass;
+use lithium\util\Set;
class SetTest extends \lithium\test\Unit {
public function testDepthWithEmptyData() {
$data = array();
$result = Set::depth($data);
- $this->assertEqual($result, 0);
+ $this->assertEqual(0, $result);
}
public function testDepthOneLevelWithDefaults() {
$data = array();
$result = Set::depth($data);
- $this->assertEqual($result, 0);
+ $this->assertEqual(0, $result);
$data = array('one', '2', 'three');
$result = Set::depth($data);
- $this->assertEqual($result, 1);
+ $this->assertEqual(1, $result);
$data = array('1' => '1.1', '2', '3');
$result = Set::depth($data);
- $this->assertEqual($result, 1);
+ $this->assertEqual(1, $result);
$data = array('1' => '1.1', '2', '3' => array('3.1' => '3.1.1'));
- $result = Set::depth($data, false, 0);
- $this->assertEqual($result, 1);
+ $result = Set::depth($data, array('all' => false));
+ $this->assertEqual(1, $result);
}
public function testDepthTwoLevelsWithDefaults() {
$data = array('1' => array('1.1' => '1.1.1'), '2', '3' => array('3.1' => '3.1.1'));
$result = Set::depth($data);
- $this->assertEqual($result, 2);
+ $this->assertEqual(2, $result);
$data = array('1' => array('1.1' => '1.1.1'), '2', '3' => array('3.1' => array(
'3.1.1' => '3.1.1.1'
@@ -54,14 +55,14 @@ class SetTest extends \lithium\test\Unit {
)),
'3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, false, 0);
+ $result = Set::depth($data, array('all' => false));
$this->assertEqual($result, 2);
}
public function testDepthTwoLevelsWithAll() {
$data = array('1' => '1.1', '2', '3' => array('3.1' => '3.1.1'));
- $result = Set::depth($data, true);
- $this->assertEqual($result, 2);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(2, $result);
}
@@ -69,57 +70,59 @@ class SetTest extends \lithium\test\Unit {
$data = array(
'1' => array('1.1' => '1.1.1'), '2', '3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, true);
- $this->assertEqual($result, 3);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(3, $result);
$data = array(
'1' => array('1.1' => '1.1.1'),
array('2' => array('2.1' => array('2.1.1' => '2.1.1.1'))),
'3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, true);
- $this->assertEqual($result, 4);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(4, $result);
$data = array(
'1' => array('1.1' => '1.1.1'),
array('2' => array('2.1' => array('2.1.1' => array('2.1.1.1')))),
'3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, true);
- $this->assertEqual($result, 5);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(5, $result);
- $data = array('1' => array('1.1' => '1.1.1'), array(
+ $data = array(
+ '1' => array('1.1' => '1.1.1'), array(
'2' => array('2.1' => array('2.1.1' => array('2.1.1.1' => '2.1.1.1.1')))),
'3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, true);
- $this->assertEqual($result, 5);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(5, $result);
}
public function testDepthFourLevelsWithAll() {
- $data = array('1' => array('1.1' => '1.1.1'), array(
+ $data = array(
+ '1' => array('1.1' => '1.1.1'), array(
'2' => array('2.1' => array('2.1.1' => '2.1.1.1'))),
'3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, true);
- $this->assertEqual($result, 4);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(4, $result);
}
public function testDepthFiveLevelsWithAll() {
-
- $data = array('1' => array('1.1' => '1.1.1'), array(
+ $data = array(
+ '1' => array('1.1' => '1.1.1'), array(
'2' => array('2.1' => array('2.1.1' => array('2.1.1.1')))),
'3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, true);
- $this->assertEqual($result, 5);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(5, $result);
$data = array('1' => array('1.1' => '1.1.1'), array(
'2' => array('2.1' => array('2.1.1' => array('2.1.1.1' => '2.1.1.1.1')))),
'3' => array('3.1' => array('3.1.1' => '3.1.1.1'))
);
- $result = Set::depth($data, true);
- $this->assertEqual($result, 5);
+ $result = Set::depth($data, array('all' => true));
+ $this->assertEqual(5, $result);
}
public function testFlattenOneLevel() {
@@ -159,7 +162,7 @@ class SetTest extends \lithium\test\Unit {
$result = Set::flatten($data);
$this->assertEqual($expected, $result);
- $result = Set::flatten(array('Post' => $data[0]['Post']), '/');
+ $result = Set::flatten(array('Post' => $data[0]['Post']), array('separator' => '/'));
$expected = array('Post/id' => '1', 'Post/author_id' => '1', 'Post/title' => 'First Post');
$this->assertEqual($expected, $result);
}
@@ -233,25 +236,25 @@ class SetTest extends \lithium\test\Unit {
array('Article' => array('id' => 3, 'title' => 'Article 3'))
);
- $this->assertTrue(Set::matches(array('id=2'), $a[1]['Article']));
- $this->assertFalse(Set::matches(array('id>2'), $a[1]['Article']));
- $this->assertTrue(Set::matches(array('id>=2'), $a[1]['Article']));
- $this->assertFalse(Set::matches(array('id>=3'), $a[1]['Article']));
- $this->assertTrue(Set::matches(array('id<=2'), $a[1]['Article']));
- $this->assertFalse(Set::matches(array('id<2'), $a[1]['Article']));
- $this->assertTrue(Set::matches(array('id>1'), $a[1]['Article']));
- $this->assertTrue(Set::matches(array('id>1', 'id<3', 'id!=0'), $a[1]['Article']));
-
- $this->assertTrue(Set::matches(array('3'), null, 3));
- $this->assertTrue(Set::matches(array('5'), null, 5));
-
- $this->assertTrue(Set::matches(array('id'), $a[1]['Article']));
- $this->assertTrue(Set::matches(array('id', 'title'), $a[1]['Article']));
- $this->assertFalse(Set::matches(array('non-existant'), $a[1]['Article']));
-
- $this->assertTrue(Set::matches('/Article[id=2]', $a));
- $this->assertFalse(Set::matches('/Article[id=4]', $a));
- $this->assertTrue(Set::matches(array(), $a));
+ $this->assertTrue(Set::matches($a[1]['Article'], array('id=2')));
+ $this->assertFalse(Set::matches($a[1]['Article'], array('id>2')));
+ $this->assertTrue(Set::matches($a[1]['Article'], array('id>=2')));
+ $this->assertFalse(Set::matches($a[1]['Article'], array('id>=3')));
+ $this->assertTrue(Set::matches($a[1]['Article'], array('id<=2')));
+ $this->assertFalse(Set::matches($a[1]['Article'], array('id<2')));
+ $this->assertTrue(Set::matches($a[1]['Article'], array('id>1')));
+ $this->assertTrue(Set::matches($a[1]['Article'], array('id>1', 'id<3', 'id!=0')));
+
+ $this->assertTrue(Set::matches(array(), array('3'), 3));
+ $this->assertTrue(Set::matches(array(), array('5'), 5));
+
+ $this->assertTrue(Set::matches($a[1]['Article'], array('id')));
+ $this->assertTrue(Set::matches($a[1]['Article'], array('id', 'title')));
+ $this->assertFalse(Set::matches($a[1]['Article'], array('non-existant')));
+
+ $this->assertTrue(Set::matches($a, '/Article[id=2]'));
+ $this->assertFalse(Set::matches($a, '/Article[id=4]'));
+ $this->assertTrue(Set::matches($a, array()));
}
public function testMatchesMultipleLevels() {
@@ -270,8 +273,7 @@ class SetTest extends \lithium\test\Unit {
)
)
);
- $result = Set::matches($result, '/Article/keep/Comment');
- $this->assertTrue($result);
+ $this->assertTrue(Set::matches($result, '/Article/keep/Comment'));
$result = Set::matches($result, '/Article/keep/Comment/fields/user');
$this->assertFalse($result);
@@ -282,9 +284,6 @@ class SetTest extends \lithium\test\Unit {
$result = Set::extract(array(), '/Post/id');
$this->assertIdentical($expected, $result);
- $result = Set::extract('/Post/id', array());
- $this->assertIdentical($expected, $result);
-
$result = Set::extract(array(
array('Post' => array('name' => 'bob')),
array('Post' => array('name' => 'jim'))
@@ -425,7 +424,7 @@ class SetTest extends \lithium\test\Unit {
);
$expected = array(array('a' => $c[2]['a']));
- $result = Set::extract('/a/II[a=3]/..', $c);
+ $result = Set::extract($c, '/a/II[a=3]/..');
$this->assertEqual($expected, $result);
$expected = array(1, 2, 3, 4, 5);
@@ -581,15 +580,15 @@ class SetTest extends \lithium\test\Unit {
'Comment' => array()
)
);
- $result = Set::extract('/', $common);
+ $result = Set::extract($common, '/');
$this->assertEqual($result, $common);
$expected = array(1);
- $result = Set::extract('/Comment/id[:first]', $common);
+ $result = Set::extract($common, '/Comment/id[:first]');
$this->assertEqual($expected, $result);
$expected = array(5);
- $result = Set::extract('/Comment/id[:last]', $common);
+ $result = Set::extract($common, '/Comment/id[:last]');
$this->assertEqual($expected, $result);
$result = Set::extract($common, '/Comment/id');
@@ -613,15 +612,15 @@ class SetTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$expected = array(array('Comment' => $common[1]['Comment'][0]));
- $result = Set::extract('/Comment[addition=]', $common);
+ $result = Set::extract($common, '/Comment[addition=]');
$this->assertEqual($expected, $result);
$expected = array(3);
- $result = Set::extract('/Article[:last]/id', $common);
+ $result = Set::extract($common, '/Article[:last]/id');
$this->assertEqual($expected, $result);
$expected = array();
- $result = Set::extract('/User/id', array());
+ $result = Set::extract(array(), '/User/id');
$this->assertEqual($expected, $result);
}
@@ -691,7 +690,9 @@ class SetTest extends \lithium\test\Unit {
$result = Set::extract($tree, '/Category[name=Category 2]');
$this->assertEqual($expected, $result);
- $expected = array(array('Category' => $tree[1]['Category'], 'children' => $tree[1]['children']));
+ $expected = array(array(
+ 'Category' => $tree[1]['Category'], 'children' => $tree[1]['children']
+ ));
$result = Set::extract($tree, '/Category[name=Category 2]/..');
$this->assertEqual($expected, $result);
@@ -884,16 +885,13 @@ class SetTest extends \lithium\test\Unit {
}
public function testMerge() {
- $result = Set::merge(array('foo'));
- $this->assertIdentical($result, array('foo'));
-
- $result = Set::merge('foo');
+ $result = Set::merge(array('foo'), array());
$this->assertIdentical($result, array('foo'));
- $result = Set::merge('foo', 'bar');
+ $result = Set::merge((array) 'foo', (array) 'bar');
$this->assertIdentical($result, array('foo', 'bar'));
- $result = Set::merge('foo', array('user' => 'bob', 'no-bar'));
+ $result = Set::merge((array) 'foo', array('user' => 'bob', 'no-bar'));
$this->assertIdentical($result, array('foo', 'user' => 'bob', 'no-bar'));
$a = array('foo', 'foo2');
@@ -906,7 +904,9 @@ class SetTest extends \lithium\test\Unit {
$a = array('users' => array('bob', 'jim'));
$b = array('users' => array('lisa', 'tina'));
- $this->assertIdentical(Set::merge($a, $b), array('users' => array('bob', 'jim', 'lisa', 'tina')));
+ $this->assertIdentical(
+ Set::merge($a, $b), array('users' => array('bob', 'jim', 'lisa', 'tina'))
+ );
$a = array('users' => array('jim', 'bob'));
$b = array('users' => 'none');
@@ -946,45 +946,52 @@ class SetTest extends \lithium\test\Unit {
$result = Set::merge($a, Set::merge($b, $c));
$this->assertIdentical($expected, $result);
- $a = array('Tree', 'CounterCache',
- 'Upload' => array('folder' => 'products',
- 'fields' => array('image_1_id', 'image_2_id', 'image_3_id', 'image_4_id', 'image_5_id')));
- $b = array('Cacheable' => array('enabled' => false),
- 'Limit',
- 'Bindable',
- 'Validator',
- 'Transactional');
-
- $expected = array('Tree', 'CounterCache',
- 'Upload' => array('folder' => 'products',
- 'fields' => array('image_1_id', 'image_2_id', 'image_3_id', 'image_4_id', 'image_5_id')),
- 'Cacheable' => array('enabled' => false),
- 'Limit',
- 'Bindable',
- 'Validator',
- 'Transactional');
+ $a = array('Tree', 'CounterCache', 'Upload' => array(
+ 'folder' => 'products', 'fields' => array(
+ 'image_1_id', 'image_2_id', 'image_3_id', 'image_4_id', 'image_5_id'
+ )
+ ));
+ $b = array(
+ 'Cacheable' => array('enabled' => false),
+ 'Limit', 'Bindable', 'Validator', 'Transactional'
+ );
+ $expected = array('Tree', 'CounterCache', 'Upload' => array(
+ 'folder' => 'products', 'fields' => array(
+ 'image_1_id', 'image_2_id', 'image_3_id', 'image_4_id', 'image_5_id'
+ )),
+ 'Cacheable' => array('enabled' => false),
+ 'Limit',
+ 'Bindable',
+ 'Validator',
+ 'Transactional'
+ );
$this->assertIdentical(Set::merge($a, $b), $expected);
- $expected = array('Tree' => null, 'CounterCache' => null,
- 'Upload' => array('folder' => 'products',
- 'fields' => array('image_1_id', 'image_2_id', 'image_3_id', 'image_4_id', 'image_5_id')),
- 'Cacheable' => array('enabled' => false),
- 'Limit' => null,
- 'Bindable' => null,
- 'Validator' => null,
- 'Transactional' => null);
-
+ $expected = array('Tree' => null, 'CounterCache' => null, 'Upload' => array(
+ 'folder' => 'products', 'fields' => array(
+ 'image_1_id', 'image_2_id', 'image_3_id', 'image_4_id', 'image_5_id'
+ )),
+ 'Cacheable' => array('enabled' => false),
+ 'Limit' => null,
+ 'Bindable' => null,
+ 'Validator' => null,
+ 'Transactional' => null
+ );
$this->assertIdentical(Set::normalize(Set::merge($a, $b)), $expected);
}
public function testSort() {
$a = array(
array('Person' => array('name' => 'Jeff'), 'Friend' => array(array('name' => 'Nate'))),
- array('Person' => array('name' => 'Tracy'),'Friend' => array(array('name' => 'Lindsay')))
+ array('Person' => array('name' => 'Tracy'), 'Friend' => array(
+ array('name' => 'Lindsay')
+ ))
);
$b = array(
- array('Person' => array('name' => 'Tracy'),'Friend' => array(array('name' => 'Lindsay'))),
+ array('Person' => array('name' => 'Tracy'),'Friend' => array(
+ array('name' => 'Lindsay')
+ )),
array('Person' => array('name' => 'Jeff'), 'Friend' => array(array('name' => 'Nate')))
);
$a = Set::sort($a, '/Friend/name', 'asc');
@@ -992,10 +999,14 @@ class SetTest extends \lithium\test\Unit {
$b = array(
array('Person' => array('name' => 'Jeff'), 'Friend' => array(array('name' => 'Nate'))),
- array('Person' => array('name' => 'Tracy'), 'Friend' => array(array('name' => 'Lindsay')))
+ array('Person' => array('name' => 'Tracy'), 'Friend' => array(
+ array('name' => 'Lindsay')
+ ))
);
$a = array(
- array('Person' => array('name' => 'Tracy'), 'Friend' => array(array('name' => 'Lindsay'))),
+ array('Person' => array('name' => 'Tracy'), 'Friend' => array(
+ array('name' => 'Lindsay')
+ )),
array('Person' => array('name' => 'Jeff'), 'Friend' => array(array('name' => 'Nate')))
);
$a = Set::sort($a, '/Friend/name', 'desc');
@@ -1003,13 +1014,17 @@ class SetTest extends \lithium\test\Unit {
$a = array(
array('Person' => array('name' => 'Jeff'), 'Friend' => array(array('name' => 'Nate'))),
- array('Person' => array('name' => 'Tracy'), 'Friend' => array(array('name' => 'Lindsay'))),
+ array('Person' => array('name' => 'Tracy'), 'Friend' => array(
+ array('name' => 'Lindsay')
+ )),
array('Person' => array('name' => 'Adam'), 'Friend' => array(array('name' => 'Bob')))
);
$b = array(
array('Person' => array('name' => 'Adam'),'Friend' => array(array('name' => 'Bob'))),
array('Person' => array('name' => 'Jeff'), 'Friend' => array(array('name' => 'Nate'))),
- array('Person' => array('name' => 'Tracy'), 'Friend' => array(array('name' => 'Lindsay')))
+ array('Person' => array('name' => 'Tracy'), 'Friend' => array(
+ array('name' => 'Lindsay')
+ ))
);
$a = Set::sort($a, '/Person/name', 'asc');
$this->assertIdentical($a, $b);
@@ -1260,7 +1275,7 @@ class SetTest extends \lithium\test\Unit {
);
$this->assertIdentical($expected, $result);
- $b = new \stdClass();
+ $b = new stdClass();
$b->users = array(
array('User' => array(
'id' => 2, 'group_id' => 1, 'Data' => array(
@@ -1283,10 +1298,7 @@ class SetTest extends \lithium\test\Unit {
$this->assertIdentical($expected, $result);
}
-
-
-
- public function testBlend() {
+ public function testAppend() {
$array1 = array('ModelOne' => array(
'id' => 1001, 'field_one' => 'a1.m1.f1', 'field_two' => 'a1.m1.f2'
));
@@ -1294,7 +1306,7 @@ class SetTest extends \lithium\test\Unit {
'id' => 1002, 'field_one' => 'a2.m2.f1', 'field_two' => 'a2.m2.f2'
));
- $result = Set::blend($array1, $array2);
+ $result = Set::append($array1, $array2);
$this->assertIdentical($result, $array1 + $array2);
@@ -1302,7 +1314,7 @@ class SetTest extends \lithium\test\Unit {
'id' => 1003, 'field_one' => 'a3.m1.f1',
'field_two' => 'a3.m1.f2', 'field_three' => 'a3.m1.f3'
));
- $result = Set::blend($array1, $array3);
+ $result = Set::append($array1, $array3);
$expected = array('ModelOne' => array(
'id' => 1001, 'field_one' => 'a1.m1.f1',
@@ -1310,7 +1322,6 @@ class SetTest extends \lithium\test\Unit {
));
$this->assertIdentical($expected, $result);
-
$array1 = array(
array('ModelOne' => array(
'id' => 1001, 'field_one' => 's1.0.m1.f1', 'field_two' => 's1.0.m1.f2'
@@ -1328,14 +1339,14 @@ class SetTest extends \lithium\test\Unit {
))
);
- $result = Set::blend($array1, $array2);
+ $result = Set::append($array1, $array2);
$this->assertIdentical($result, $array1);
$array3 = array(array('ModelThree' => array(
'id' => 1003, 'field_one' => 's3.0.m3.f1', 'field_two' => 's3.0.m3.f2'
)));
- $result = Set::blend($array1, $array3);
+ $result = Set::append($array1, $array3);
$expected = array(
array(
'ModelOne' => array(
@@ -1351,730 +1362,34 @@ class SetTest extends \lithium\test\Unit {
);
$this->assertIdentical($expected, $result);
- $result = Set::blend($array1, null);
+ $result = Set::append($array1, array());
$this->assertIdentical($result, $array1);
- $result = Set::blend($array1, $array2);
+ $result = Set::append($array1, $array2);
$this->assertIdentical($result, $array1 + $array2);
+
+ $result = Set::append(array(), array('2'));
+ $this->assertIdentical(array('2'), $result);
}
public function testStrictKeyCheck() {
$set = array('a' => 'hi');
$this->assertFalse(Set::check($set, 'a.b'));
+ $this->assertTrue(Set::check($set, 'a'));
}
public function testMixedKeyNormalization() {
$input = array('"string"' => array('before' => '=>'), 1 => array('before' => '=>'));
$result = Set::normalize($input);
$this->assertEqual($input, $result);
- }
-
- public function testToArrayNullAndFalse() {
- $result = Set::to('array', null);
- $this->assertEqual(null, $result);
-
- $result = Set::to('array', false);
- $this->assertEqual(false, $result);
- }
-
- public function testToArrayFromObject() {
- $expected = array('User' => array('psword'=> 'whatever', 'Icon' => array('id' => 851)));
- $class = new \stdClass;
- $class->User = new \stdClass;
- $class->User->psword = 'whatever';
- $class->User->Icon = new \stdClass;
- $class->User->Icon->id = 851;
- $result = Set::to('array', $class);
- $this->assertIdentical($expected, $result);
-
- $expected = array(
- 'User' => array(
- 'psword'=> 'whatever', 'Icon' => array('id' => 851),
- 'Profile' => array('name' => 'Some Name', 'address' => 'Some Address')
- )
- );
- $class = new \stdClass;
- $class->User = new \stdClass;
- $class->User->psword = 'whatever';
- $class->User->Icon = new \stdClass;
- $class->User->Icon->id = 851;
- $class->User->Profile = new \stdClass;
- $class->User->Profile->name = 'Some Name';
- $class->User->Profile->address = 'Some Address';
-
- $result = Set::to('array', $class);
- $this->assertIdentical($expected, $result);
-
- $expected = array('User' => array(
- 'psword'=> 'whatever',
- 'Icon' => array('id'=> 851),
- 'Profile' => array('name' => 'Some Name', 'address' => 'Some Address'),
- 'Comment' => array(
- array(
- 'id' => 1, 'article_id' => 1, 'user_id' => 1,
- 'comment' => 'First Comment for First Article', 'published' => 'Y',
- 'created' => '2007-03-18 10:47:23', 'updated' => '2007-03-18 10:49:31'
- ),
- array(
- 'id' => 2, 'article_id' => 1, 'user_id' => 2,
- 'comment' => 'Second Comment for First Article', 'published' => 'Y',
- 'created' => '2007-03-18 10:47:23', 'updated' => '2007-03-18 10:49:31'
- )
- )
- ));
-
- $class = new \stdClass;
- $class->User = new \stdClass;
- $class->User->psword = 'whatever';
- $class->User->Icon = new \stdClass;
- $class->User->Icon->id = 851;
- $class->User->Profile = new \stdClass;
- $class->User->Profile->name = 'Some Name';
- $class->User->Profile->address = 'Some Address';
- $class->User->Comment = new \stdClass;
- $class->User->Comment->{'0'} = new \stdClass;
- $class->User->Comment->{'0'}->id = 1;
- $class->User->Comment->{'0'}->article_id = 1;
- $class->User->Comment->{'0'}->user_id = 1;
- $class->User->Comment->{'0'}->comment = 'First Comment for First Article';
- $class->User->Comment->{'0'}->published = 'Y';
- $class->User->Comment->{'0'}->created = '2007-03-18 10:47:23';
- $class->User->Comment->{'0'}->updated = '2007-03-18 10:49:31';
- $class->User->Comment->{'1'} = new \stdClass;
- $class->User->Comment->{'1'}->id = 2;
- $class->User->Comment->{'1'}->article_id = 1;
- $class->User->Comment->{'1'}->user_id = 2;
- $class->User->Comment->{'1'}->comment = 'Second Comment for First Article';
- $class->User->Comment->{'1'}->published = 'Y';
- $class->User->Comment->{'1'}->created = '2007-03-18 10:47:23';
- $class->User->Comment->{'1'}->updated = '2007-03-18 10:49:31';
-
- $result = Set::to('array', $class);
- $this->assertIdentical($expected, $result);
-
- $expected = array('User' => array(
- 'psword'=> 'whatever',
- 'Icon' => array('id'=> 851),
- 'Profile' => array('name' => 'Some Name', 'address' => 'Some Address'),
- 'Comment' => array(
- array(
- 'id' => 1, 'article_id' => 1, 'user_id' => 1,
- 'comment' => 'First Comment for First Article', 'published' => 'Y',
- 'created' => '2007-03-18 10:47:23', 'updated' => '2007-03-18 10:49:31'
- ),
- array(
- 'id' => 2, 'article_id' => 1, 'user_id' => 2,
- 'comment' => 'Second Comment for First Article', 'published' => 'Y',
- 'created' => '2007-03-18 10:47:23', 'updated' => '2007-03-18 10:49:31'
- )
- )
- ));
-
- $class = new \stdClass;
- $class->User = new \stdClass;
- $class->User->psword = 'whatever';
- $class->User->Icon = new \stdClass;
- $class->User->Icon->id = 851;
- $class->User->Profile = new \stdClass;
- $class->User->Profile->name = 'Some Name';
- $class->User->Profile->address = 'Some Address';
- $class->User->Comment = array();
- $comment = new \stdClass;
- $comment->id = 1;
- $comment->article_id = 1;
- $comment->user_id = 1;
- $comment->comment = 'First Comment for First Article';
- $comment->published = 'Y';
- $comment->created = '2007-03-18 10:47:23';
- $comment->updated = '2007-03-18 10:49:31';
- $comment2 = new \stdClass;
- $comment2->id = 2;
- $comment2->article_id = 1;
- $comment2->user_id = 2;
- $comment2->comment = 'Second Comment for First Article';
- $comment2->published = 'Y';
- $comment2->created = '2007-03-18 10:47:23';
- $comment2->updated = '2007-03-18 10:49:31';
- $class->User->Comment = array($comment, $comment2);
- $result = Set::to('array', $class);
- $this->assertIdentical($expected, $result);
-
- $class = new \stdClass;
- $class->User = new \stdClass;
- $class->User->id = 100;
- $class->someString = 'this is some string';
- $class->Profile = new \stdClass;
- $class->Profile->name = 'Joe Mamma';
-
- $result = Set::to('array', $class);
- $expected = array(
- 'User' => array('id' => '100'),
- 'someString' => 'this is some string',
- 'Profile' => array('name' => 'Joe Mamma')
- );
- $this->assertEqual($expected, $result);
-
- $class = new \stdClass;
- $class->User = new \stdClass;
- $class->User->id = 100;
- $class->User->_name_ = 'User';
- $class->Profile = new \stdClass;
- $class->Profile->name = 'Joe Mamma';
- $class->Profile->_name_ = 'Profile';
-
- $result = Set::to('array', $class);
- $expected = array(
- 'User' => array('id' => '100'),
- 'Profile' => array('name' => 'Joe Mamma')
- );
- $this->assertEqual($expected, $result);
- }
- public function testAssociativeArrayToObject() {
- $data =array(
- 'Post' => array(
- 'id' => '1', 'author_id' => '1', 'title' => 'First Post',
- 'body' => 'First Post Body', 'published' => 'Y',
- 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31'
- ),
- 'Author' => array(
- 'id' => '1', 'user' => 'mariano',
- 'password' => '5f4dcc3b5aa765d61d8327deb882cf99',
- 'created' => '2007-03-17 01:16:23',
- 'updated' => '2007-03-17 01:18:31',
- 'test' => 'working'
- ),
- );
- $result = Set::to('object', $data);
- $expected = new \stdClass;
- $expected->_name_ = 'Post';
- $expected->id = '1';
- $expected->author_id = '1';
- $expected->title = 'First Post';
- $expected->body = 'First Post Body';
- $expected->published = 'Y';
- $expected->created = "2007-03-18 10:39:23";
- $expected->updated = "2007-03-18 10:41:31";
-
- $expected->Author = new \stdClass;
- $expected->Author->id = '1';
- $expected->Author->user = 'mariano';
- $expected->Author->password = '5f4dcc3b5aa765d61d8327deb882cf99';
- $expected->Author->created = "2007-03-17 01:16:23";
- $expected->Author->updated = "2007-03-17 01:18:31";
- $expected->Author->test = "working";
- $expected->Author->_name_ = 'Author';
- $this->assertEqual($expected, $result);
- }
-
- public function testNestedArrayToObject() {
- $data = array(
- array(
- 'Post' => array(
- 'id' => '1', 'author_id' => '1', 'title' => 'First Post',
- 'body' => 'First Post Body', 'published' => 'Y',
- 'created' => '2007-03-18 10:39:23',
- 'updated' => '2007-03-18 10:41:31'
- ),
- 'Author' => array(
- 'id' => '1', 'user' => 'mariano',
- 'password' => '5f4dcc3b5aa765d61d8327deb882cf99',
- 'created' => '2007-03-17 01:16:23',
- 'updated' => '2007-03-17 01:18:31',
- 'test' => 'working'
- ),
- ),
- array(
- 'Post' => array(
- 'id' => '2', 'author_id' => '3', 'title' => 'Second Post',
- 'body' => 'Second Post Body', 'published' => 'Y',
- 'created' => '2007-03-18 10:41:23',
- 'updated' => '2007-03-18 10:43:31'
- ),
- 'Author' => array(
- 'id' => '3', 'user' => 'joel',
- 'password' => '5f4dcc3b5aa765d61d8327deb882cf99',
- 'created' => '2007-03-17 01:20:23',
- 'updated' => '2007-03-17 01:22:31',
- 'test' => 'working'
- ),
- )
- );
- $result = Set::to('object', $data);
-
- $expected = new \stdClass;
- $expected->_name_ = 'Post';
- $expected->id = '1';
- $expected->author_id = '1';
- $expected->title = 'First Post';
- $expected->body = 'First Post Body';
- $expected->published = 'Y';
- $expected->created = "2007-03-18 10:39:23";
- $expected->updated = "2007-03-18 10:41:31";
-
- $expected->Author = new \stdClass;
- $expected->Author->id = '1';
- $expected->Author->user = 'mariano';
- $expected->Author->password = '5f4dcc3b5aa765d61d8327deb882cf99';
- $expected->Author->created = "2007-03-17 01:16:23";
- $expected->Author->updated = "2007-03-17 01:18:31";
- $expected->Author->test = "working";
- $expected->Author->_name_ = 'Author';
-
- $expected2 = new \stdClass;
- $expected2->_name_ = 'Post';
- $expected2->id = '2';
- $expected2->author_id = '3';
- $expected2->title = 'Second Post';
- $expected2->body = 'Second Post Body';
- $expected2->published = 'Y';
- $expected2->created = "2007-03-18 10:41:23";
- $expected2->updated = "2007-03-18 10:43:31";
-
- $expected2->Author = new \stdClass;
- $expected2->Author->id = '3';
- $expected2->Author->user = 'joel';
- $expected2->Author->password = '5f4dcc3b5aa765d61d8327deb882cf99';
- $expected2->Author->created = "2007-03-17 01:20:23";
- $expected2->Author->updated = "2007-03-17 01:22:31";
- $expected2->Author->test = "working";
- $expected2->Author->_name_ = 'Author';
-
- $test = array();
- $test[0] = $expected;
- $test[1] = $expected2;
-
- $this->assertEqual($test, $result);
- }
-
- public function testDeepNestedArrayToObject() {
- $data = array(
- 'User' => array(
- 'id' => 1,
- 'email' => 'user@example.com',
- 'first_name' => 'John',
- 'last_name' => 'Smith',
- ),
- 'Piece' => array(
- array(
- 'id' => 1,
- 'title' => 'Moonlight Sonata',
- 'composer' => 'Ludwig van Beethoven',
- 'PiecesUser' => array(
- 'id' => 1,
- 'created' => '2008-01-01 00:00:00',
- 'modified' => '2008-01-01 00:00:00',
- 'piece_id' => 1,
- 'user_id' => 2,
- )
- ),
- array(
- 'id' => 2,
- 'title' => 'Moonlight Sonata 2',
- 'composer' => 'Ludwig van Beethoven',
- 'PiecesUser' => array(
- 'id' => 2,
- 'created' => '2008-01-01 00:00:00',
- 'modified' => '2008-01-01 00:00:00',
- 'piece_id' => 2,
- 'user_id' => 2,
- )
- )
- )
- );
-
- $result = Set::to('object', $data);
-
- $expected = new \stdClass();
- $expected->_name_ = 'User';
- $expected->id = 1;
- $expected->email = 'user@example.com';
- $expected->first_name = 'John';
- $expected->last_name = 'Smith';
-
- $piece = new \stdClass();
- $piece->id = 1;
- $piece->title = 'Moonlight Sonata';
- $piece->composer = 'Ludwig van Beethoven';
-
- $piece->PiecesUser = new \stdClass();
- $piece->PiecesUser->id = 1;
- $piece->PiecesUser->created = '2008-01-01 00:00:00';
- $piece->PiecesUser->modified = '2008-01-01 00:00:00';
- $piece->PiecesUser->piece_id = 1;
- $piece->PiecesUser->user_id = 2;
- $piece->PiecesUser->_name_ = 'PiecesUser';
-
- $piece->_name_ = 'Piece';
-
-
- $piece2 = new \stdClass();
- $piece2->id = 2;
- $piece2->title = 'Moonlight Sonata 2';
- $piece2->composer = 'Ludwig van Beethoven';
-
- $piece2->PiecesUser = new \stdClass();
- $piece2->PiecesUser->id = 2;
- $piece2->PiecesUser->created = '2008-01-01 00:00:00';
- $piece2->PiecesUser->modified = '2008-01-01 00:00:00';
- $piece2->PiecesUser->piece_id = 2;
- $piece2->PiecesUser->user_id = 2;
- $piece2->PiecesUser->_name_ = 'PiecesUser';
-
- $piece2->_name_ = 'Piece';
-
- $expected->Piece = array($piece, $piece2);
-
- $this->assertEqual($expected, $result);
- }
-
- public function testToObjectNullValue() {
- $expected = null;
- $result = Set::to('object', null);
- $this->assertEqual($expected, $result);
- }
-
- public function testToObjectWithTypicalArray() {
- $expected = array(
- 'Post' => array('id'=> 1, 'title' => 'First Post'),
- 'Comment' => array(
- array('id'=> 1, 'title' => 'First Comment'),
- array('id'=> 2, 'title' => 'Second Comment')
- ),
- 'Tag' => array(
- array('id'=> 1, 'title' => 'First Tag'),
- array('id'=> 2, 'title' => 'Second Tag')
- ),
- );
- $map = Set::to('object', $expected);
- $this->assertIdentical($map->title, $expected['Post']['title']);
- foreach ($map->Comment as $comment) {
- $ids[] = $comment->id;
- }
- $this->assertIdentical($ids, array(1, 2));
-
- $expected = array('User' => array('psword'=> 'whatever', 'Icon' => array('id' => 851)));
- $map = Set::to('object', $expected);
- $result = Set::to('array', $map);
- $this->assertIdentical($expected, $result);
- }
-
- public function testToObjectWithPredifinedName() {
- $data = array(
- 'User' => array(
- 'id' => 1,
- 'email' => 'user@example.com',
- 'first_name' => 'John',
- 'last_name' => 'Smith',
- '_name_' => 'FooUser',
- ),
- 'Piece' => array(
- array(
- 'id' => 1,
- 'title' => 'Moonlight Sonata',
- 'composer' => 'Ludwig van Beethoven',
- '_name_' => 'FooPiece',
- 'PiecesUser' => array(
- 'id' => 1,
- 'created' => '2008-01-01 00:00:00',
- 'modified' => '2008-01-01 00:00:00',
- 'piece_id' => 1,
- 'user_id' => 2,
- '_name_' => 'FooPiecesUser',
- )
- ),
- array(
- 'id' => 2,
- 'title' => 'Moonlight Sonata 2',
- 'composer' => 'Ludwig van Beethoven',
- '_name_' => 'FooPiece',
- 'PiecesUser' => array(
- 'id' => 2,
- 'created' => '2008-01-01 00:00:00',
- 'modified' => '2008-01-01 00:00:00',
- 'piece_id' => 2,
- 'user_id' => 2,
- '_name_' => 'FooPiecesUser',
- )
- )
- )
- );
-
- $result = Set::to('object', $data);
-
- $expected = new \stdClass();
- $expected->_name_ = 'FooUser';
- $expected->id = 1;
- $expected->email = 'user@example.com';
- $expected->first_name = 'John';
- $expected->last_name = 'Smith';
-
- $piece = new \stdClass();
- $piece->id = 1;
- $piece->title = 'Moonlight Sonata';
- $piece->composer = 'Ludwig van Beethoven';
- $piece->_name_ = 'FooPiece';
- $piece->PiecesUser = new \stdClass();
- $piece->PiecesUser->id = 1;
- $piece->PiecesUser->created = '2008-01-01 00:00:00';
- $piece->PiecesUser->modified = '2008-01-01 00:00:00';
- $piece->PiecesUser->piece_id = 1;
- $piece->PiecesUser->user_id = 2;
- $piece->PiecesUser->_name_ = 'FooPiecesUser';
-
- $piece2 = new \stdClass();
- $piece2->id = 2;
- $piece2->title = 'Moonlight Sonata 2';
- $piece2->composer = 'Ludwig van Beethoven';
- $piece2->_name_ = 'FooPiece';
- $piece2->PiecesUser = new \stdClass();
- $piece2->PiecesUser->id = 2;
- $piece2->PiecesUser->created = '2008-01-01 00:00:00';
- $piece2->PiecesUser->modified = '2008-01-01 00:00:00';
- $piece2->PiecesUser->piece_id = 2;
- $piece2->PiecesUser->user_id = 2;
- $piece2->PiecesUser->_name_ = 'FooPiecesUser';
-
- $expected->Piece = array($piece, $piece2);
-
- $this->assertEqual($expected, $result);
- }
-
- public function testComplexArrayToObjectBackToArray() {
- $expected = array(
- array(
- "IndexedPage" => array(
- "id" => 1,
- "url" => 'http://blah.com/',
- 'hash' => '68a9f053b19526d08e36c6a9ad150737933816a5',
- 'headers' => array(
- 'Date' => "Wed, 14 Nov 2007 15:51:42 GMT",
- 'Server' => "Apache",
- 'Expires' => "Thu, 19 Nov 1981 08:52:00 GMT",
- 'Cache-Control' => "private",
- 'Pragma' => "no-cache",
- 'Content-Type' => "text/html; charset=UTF-8",
- 'X-Original-Transfer-Encoding' => "chunked",
- 'Content-Length' => "50210",
- ),
- 'meta' => array(
- 'keywords' => array('testing','tests'),
- 'description'=>'describe me',
- ),
- 'get_vars' => '',
- 'post_vars' => array(),
- 'cookies' => array('PHPSESSID' => "dde9896ad24595998161ffaf9e0dbe2d"),
- 'redirect' => '',
- 'created' => "1195055503",
- 'updated' => "1195055503",
- )
- ),
- array(
- "IndexedPage" => array(
- "id" => 2,
- "url" => 'http://blah.com/',
- 'hash' => '68a9f053b19526d08e36c6a9ad150737933816a5',
- 'headers' => array(
- 'Date' => "Wed, 14 Nov 2007 15:51:42 GMT",
- 'Server' => "Apache",
- 'Expires' => "Thu, 19 Nov 1981 08:52:00 GMT",
- 'Cache-Control' => "private",
- 'Pragma' => "no-cache",
- 'Content-Type' => "text/html; charset=UTF-8",
- 'X-Original-Transfer-Encoding' => "chunked",
- 'Content-Length' => "50210",
- ),
- 'meta' => array(
- 'keywords' => array('testing','tests'),
- 'description'=>'describe me',
- ),
- 'get_vars' => '',
- 'post_vars' => array(),
- 'cookies' => array('PHPSESSID' => "dde9896ad24595998161ffaf9e0dbe2d"),
- 'redirect' => '',
- 'created' => "1195055503",
- 'updated' => "1195055503",
- ),
- )
- );
-
- $mapped = Set::to('object', $expected);
- $ids = array();
-
- foreach ($mapped as $object) {
- $ids[] = $object->id;
- }
- $this->assertEqual($ids, array(1, 2));
- $this->assertEqual(
- get_object_vars($mapped[0]->headers),
- $expected[0]['IndexedPage']['headers']
- );
-
- $result = Set::to('array', $mapped);
- $this->assertIdentical($expected, $result);
-
- $data = array(
- array(
- "IndexedPage" => array(
- "id" => 1,
- "url" => 'http://blah.com/',
- 'hash' => '68a9f053b19526d08e36c6a9ad150737933816a5',
- 'get_vars' => '',
- 'redirect' => '',
- 'created' => "1195055503",
- 'updated' => "1195055503",
- )
- ),
- array(
- "IndexedPage" => array(
- "id" => 2,
- "url" => 'http://blah.com/',
- 'hash' => '68a9f053b19526d08e36c6a9ad150737933816a5',
- 'get_vars' => '',
- 'redirect' => '',
- 'created' => "1195055503",
- 'updated' => "1195055503",
- ),
- )
- );
- $mapped = Set::to('object', $data);
-
- $expected = new \stdClass();
- $expected->_name_ = 'IndexedPage';
- $expected->id = 2;
- $expected->url = 'http://blah.com/';
- $expected->hash = '68a9f053b19526d08e36c6a9ad150737933816a5';
- $expected->get_vars = '';
- $expected->redirect = '';
- $expected->created = "1195055503";
- $expected->updated = "1195055503";
- $this->assertEqual($mapped[1], $expected);
-
- $ids = array();
-
- foreach ($mapped as $object) {
- $ids[] = $object->id;
- }
- $this->assertEqual($ids, array(1, 2));
- }
-
- public function testMixedArrayToObjectAndBackToArrayNotFlattened() {
- $expected = array(
- 'Array1' => array(
- 'Array1Data1' => 'Array1Data1 value 1', 'Array1Data2' => 'Array1Data2 value 2'
- ),
- 'Array2' => array(
- array(
- 'Array2Data1' => 1, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 2, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 3, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 4, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 5, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- )
- ),
- 'Array3' => array(
- array(
- 'Array3Data1' => 1, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 2, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 3, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 4, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 5, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- )
- )
- );
- $map = Set::to('object', $expected, array('flatten' => false));
- $this->assertEqual($map->Array1->Array1Data1, $expected['Array1']['Array1Data1']);
- $this->assertEqual($map->Array2[0]->Array2Data1, $expected['Array2'][0]['Array2Data1']);
-
- $result = Set::to('array', $map);
- $this->assertEqual($expected, $result);
- }
+ $input = 'Foo,Bar,Baz';
+ $result = Set::normalize($input);
+ $this->assertEqual(array('Foo' => null, 'Bar' => null, 'Baz' => null), $result);
- public function testToObjectAndToArrayWithSomeRandomArraysNotFlattened() {
- $expected = array(
- 'Array1' => array(
- 'Array1Data1' => 'Array1Data1 value 1', 'Array1Data2' => 'Array1Data2 value 2',
- 'Array1Data3' => 'Array1Data3 value 3','Array1Data4' => 'Array1Data4 value 4',
- 'Array1Data5' => 'Array1Data5 value 5', 'Array1Data6' => 'Array1Data6 value 6',
- 'Array1Data7' => 'Array1Data7 value 7', 'Array1Data8' => 'Array1Data8 value 8'
- ),
- 'string' => 1,
- 'another' => 'string',
- 'some' => 'thing else',
- 'Array2' => array(
- array(
- 'Array2Data1' => 1, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 2, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 3, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 4, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- ),
- array(
- 'Array2Data1' => 5, 'Array2Data2' => 'Array2Data2 value 2',
- 'Array2Data3' => 'Array2Data3 value 2', 'Array2Data4' => 'Array2Data4 value 4'
- )
- ),
- 'Array3' => array(
- array(
- 'Array3Data1' => 1, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 2, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 3, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 4, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- ),
- array(
- 'Array3Data1' => 5, 'Array3Data2' => 'Array3Data2 value 2',
- 'Array3Data3' => 'Array3Data3 value 2', 'Array3Data4' => 'Array3Data4 value 4'
- )
- )
- );
- $map = Set::to('object', $expected, array('flatten' => false));
- $result = Set::to('array', $map);
- $this->assertIdentical($expected, $result);
+ $input = array('baz' => 'foo', 'bar');
+ $result = Set::normalize($input, false);
+ $this->assertEqual(array('baz' => 'foo', 'bar' => null), $result);
}
}
diff --git a/libraries/lithium/tests/cases/util/StringTest.php b/libraries/lithium/tests/cases/util/StringTest.php
index ef8e1f3..faa6e2a 100644
--- a/libraries/lithium/tests/cases/util/StringTest.php
+++ b/libraries/lithium/tests/cases/util/StringTest.php
@@ -2,17 +2,36 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\util;
-use \lithium\util\String;
-use \lithium\net\http\Request;
-use \lithium\tests\mocks\util\MockStringObject;
+use lithium\util\String;
+use lithium\net\http\Request;
+use lithium\tests\mocks\util\MockStringObject;
class StringTest extends \lithium\test\Unit {
+ /**
+ * testRandomGenerator method
+ *
+ * @return void
+ */
+ public function testRandomGenerator() {
+ // Disallow allow seeding twice
+ $this->assertFalse(String::seed() && String::seed());
+
+ $check = array();
+ $count = 50;
+ $pattern = "/^[0-9A-Za-z\.\/]+$/";
+ for ($i = 0; $i < $count; $i++) {
+ $result = String::random(8);
+ $this->assertPattern($pattern, String::encode64($result));
+ $this->assertFalse(in_array($result, $check));
+ $check[] = $result;
+ }
+ }
/**
* testUuidGeneration method
@@ -20,11 +39,11 @@ class StringTest extends \lithium\test\Unit {
* @return void
*/
public function testUuidGeneration() {
- $result = String::uuid(new Request());
- $pattern = "/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/";
+ $result = String::uuid();
+ $pattern = "/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[8-9a-b][a-f0-9]{3}-[a-f0-9]{12}$/";
$this->assertPattern($pattern, $result);
- $result = String::uuid($_SERVER);
+ $result = String::uuid();
$this->assertPattern($pattern, $result);
}
@@ -35,11 +54,11 @@ class StringTest extends \lithium\test\Unit {
*/
public function testMultipleUuidGeneration() {
$check = array();
- $count = 500;
- $pattern = "/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/";
+ $count = 50;
+ $pattern = "/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[8-9a-b][a-f0-9]{3}-[a-f0-9]{12}$/";
for ($i = 0; $i < $count; $i++) {
- $result = String::uuid($_SERVER);
+ $result = String::uuid();
$match = preg_match($pattern, $result);
$this->assertTrue($match);
$this->assertFalse(in_array($result, $check));
@@ -48,81 +67,76 @@ class StringTest extends \lithium\test\Unit {
}
/**
- * Tests generating a UUID with seed data provided by an anonymous function.
- *
- * @return void
- */
- public function testGeneratingUuidWithCallback() {
- $pattern = "/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/";
-
- $result = String::uuid(function($value) {
- if ($value == 'SERVER_ADDR') {
- return '::1';
- }
- });
- $this->assertPattern($pattern, $result);
-
- $result = String::uuid(function($value) {
- if ($value == 'HOST') {
- return '127.0.0.1';
- }
- });
- $this->assertPattern($pattern, $result);
-
- $result = String::uuid(function($value) {
- if ($value == 'SERVER_ADDR') {
- return '127.0.0.2';
- }
- });
- $this->assertPattern($pattern, $result);
- }
-
- /**
* testHash method - Tests hash generation using `util\String::hash()`
*
* @return void
*/
public function testHash() {
- $salt = 'Salt and pepper';
- $value = 'Lithium rocks!';
+ $string = 'Hash Me';
+ $key = 'a very valid key';
+ $salt = 'not too much';
+ $type = 'sha256';
- $expected = sha1($value);
- $result = String::hash($value, 'sha1');
- $this->assertEqual($expected, $result);
+ $expected = '24f8664f7a7e56f85bd5c983634aaa0b0d3b0e470d7f63494475729cb8b3c6a4ef28398d7cf3';
+ $expected .= '780c0caec26c85b56a409920e4af7eef38597861d49fbe31b9a0';
- $result = String::hash($value);
+ $result = String::hash($string, compact('key'));
$this->assertEqual($expected, $result);
- $expected = sha1($salt . $value);
- $result = String::hash($value, 'sha1', $salt);
+ $expected = '35bc1d9a3332e524962909b7ccff6b34ae143f64c48ffa32b5be9312719a96369fbd7ebf6f49';
+ $expected .= '09b375135b34e28b063a07b5bd62af165483c6b80dd48a252ddd';
+
+ $result = String::hash($string, compact('salt'));
$this->assertEqual($expected, $result);
- $expected = md5($value);
- $result = String::hash($value, 'md5');
+ $expected = 'fa4cfa5c16d7f94e221e1d3a0cb01eadfd6823d68497a5fdcae023d24f557e4a';
+ $result = String::hash($string, compact('type', 'key'));
$this->assertEqual($expected, $result);
- $expected = md5($salt . $value);
- $result = String::hash($value, 'md5', $salt);
+ $expected = 'a9050b4f44797bf60262de984ca12967711389cd6c4c4aeee2a739c159f1f667';
+ $result = String::hash($string, compact('type'));
$this->assertEqual($expected, $result);
+ }
- $sha256 = function($value) {
- if (function_exists('mhash')) {
- return bin2hex(mhash(MHASH_SHA256, $value));
- } elseif (function_exists('hash')) {
- return hash('sha256', $value);
+ /**
+ * testPassword method
+ *
+ * @return void
+ **/
+ public function testPassword() {
+ $pass = 'Lithium rocks!';
+
+ $bfSalt = "{^\\$2a\\$06\\$[0-9A-Za-z./]{22}$}";
+ $bfHash = "{^\\$2a\\$06\\$[0-9A-Za-z./]{53}$}";
+
+ $xdesSalt = "{^_zD..[0-9A-Za-z./]{4}$}";
+ $xdesHash = "{^_zD..[0-9A-Za-z./]{15}$}";
+
+ $md5Salt = "{^\\$1\\$[0-9A-Za-z./]{8}$}";
+ $md5Hash = "{^\\$1\\$[0-9A-Za-z./]{8}\\$[0-9A-Za-z./]{22}$}";
+
+ // Make it a bit slow, else we'll be there tomorrow
+ foreach (array('bf' => 6, 'xdes' => 10, 'md5' => false) as $method => $log2) {
+ $salts = array();
+ $hashes = array();
+ $count = 50;
+ $saltPattern = ${$method . 'Salt'};
+ $hashPattern = ${$method . 'Hash'};
+
+ for ($i = 0; $i < $count; $i++) {
+ $salt = String::genSalt($method, $log2);
+ $this->assertPattern($saltPattern, $salt);
+ $this->assertFalse(in_array($salt, $salts));
+ $salts[] = $salt;
+
+ $hash = String::hashPassword($pass, $salt);
+ $this->assertPattern($hashPattern, $hash);
+ $this->assertEqual(substr($hash, 0, strlen($salt)), $salt);
+ $this->assertFalse(in_array($hash, $hashes));
+ $hashes[] = $hash;
+
+ $this->assertTrue(String::checkPassword($pass, $hash));
}
- throw new Exception();
- };
-
- try {
- $expected = $sha256($value);
- $result = String::hash($value, 'sha256');
- $this->assertEqual($expected, $result);
-
- $expected = $sha256($salt . $value);
- $result = String::hash($value, 'sha256', $salt);
- $this->assertEqual($expected, $result);
- } catch (Exception $e) {
}
}
@@ -417,8 +431,7 @@ class StringTest extends \lithium\test\Unit {
$this->assertEqual($expected, $result);
$result = String::tokenize(null);
- $expected = null;
- $this->assertEqual($expected, $result);
+ $this->assertNull($result);
}
/**
diff --git a/libraries/lithium/tests/cases/util/ValidatorTest.php b/libraries/lithium/tests/cases/util/ValidatorTest.php
index af9d451..ffbb769 100644
--- a/libraries/lithium/tests/cases/util/ValidatorTest.php
+++ b/libraries/lithium/tests/cases/util/ValidatorTest.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\util;
-use \lithium\util\Validator;
+use lithium\util\Validator;
class ValidatorTest extends \lithium\test\Unit {
@@ -30,6 +30,40 @@ class ValidatorTest extends \lithium\test\Unit {
$this->assertTrue(Validator::isUrl('google.com', 'loose'));
}
+ public function testFieldOption() {
+ Validator::add('isInArray', function($data, $params, $options) {
+ $existing = array(
+ 'number' => array('one', 'two', 'three'),
+ 'name' => array('bob', 'bill')
+ );
+ return isset($options['field']) && isset($existing[$options['field']]) &&
+ in_array($data,$existing[$options['field']]);
+ });
+
+ $fieldValidationRules = array(
+ 'number' => array('rule' => array('isInArray')),
+ 'name' => array('rule' => array('isInArray')),
+ );
+
+ $result = Validator::check(
+ array('number' => 'one', 'name' => 'bob'),
+ $fieldValidationRules
+ );
+ $this->assertTrue(empty($result));
+
+ $result = Validator::check(
+ array('number' => 'four', 'name' => 'bob'),
+ $fieldValidationRules
+ );
+ $this->assertFalse(empty($result));
+
+ $result = Validator::check(
+ array('number' => 'one', 'name' => 'rex'),
+ $fieldValidationRules
+ );
+ $this->assertFalse(empty($result));
+ }
+
/**
* Tests that new methods can be called on Validator by adding rules using Validator::add().
*
@@ -44,7 +78,7 @@ class ValidatorTest extends \lithium\test\Unit {
$this->assertTrue(in_array('foo', Validator::rules()));
$this->assertEqual('/^foo$/', Validator::rules('foo'));
- $this->expectException("Rule 'bar' is not a validation rule");
+ $this->expectException("Rule `bar` is not a validation rule.");
$this->assertNull(Validator::isBar('foo'));
}
@@ -73,6 +107,7 @@ class ValidatorTest extends \lithium\test\Unit {
$this->assertTrue(Validator::isUuid('1c0a5831-6025-11de-8a39-0800200c9a66'));
$this->assertTrue(Validator::isUuid('1c0a5832-6025-11de-8a39-0800200c9a66'));
$this->assertFalse(Validator::isUuid('zc0a5832-6025-11de-8a39-0800200c9a66'));
+ $this->assertFalse(Validator::isUuid('1-1c0a5832-6025-11de-8a39-0800200c9a66'));
}
/**
@@ -197,6 +232,12 @@ class ValidatorTest extends \lithium\test\Unit {
* @return void
*/
function testDecimal() {
+ $this->assertTrue(Validator::isDecimal('0.0'));
+ $this->assertTrue(Validator::isDecimal('0.000'));
+ $this->assertTrue(Validator::isDecimal('1.1'));
+ $this->assertTrue(Validator::isDecimal('11.11'));
+ $this->assertTrue(Validator::isDecimal('+0'));
+ $this->assertTrue(Validator::isDecimal('-0'));
$this->assertTrue(Validator::isDecimal('+1234.54321'));
$this->assertTrue(Validator::isDecimal('-1234.54321'));
$this->assertTrue(Validator::isDecimal('1234.54321'));
@@ -280,13 +321,25 @@ class ValidatorTest extends \lithium\test\Unit {
* Strange, but valid addresses
*/
$this->assertTrue(Validator::isEmail('_somename@example.com'));
- $this->assertTrue(Validator::isEmail('abc@example'));
$this->assertTrue(Validator::isEmail('abc@example.c'));
- $this->assertTrue(Validator::isEmail('abc.@example.com'));
$this->assertTrue(Validator::isEmail('abc@example.com.a'));
$this->assertTrue(Validator::isEmail('abc@example.toolong'));
/**
+ * Addresses which are invalid, but not caught until PHP 5.3.3.
+ */
+ $this->assertFalse(
+ Validator::isEmail('abc@example'),
+ 'Invalid email address passed validation. Please update to PHP 5.3.3 ' .
+ 'or higher to correct this.'
+ );
+ $this->assertFalse(
+ Validator::isEmail('abc.@example.com'),
+ 'Invalid email address passed validation. Please update to PHP 5.3.3 ' .
+ 'or higher to correct this.'
+ );
+
+ /**
* Invalid addresses
*/
$this->assertFalse(Validator::isEmail('abc@example.com.'));
@@ -312,6 +365,8 @@ class ValidatorTest extends \lithium\test\Unit {
* @return void
*/
public function testEmailDomainCheck() {
+ $this->skipIf(dns_check_record("google.com") === false, "No internet connection.");
+
$this->assertTrue(Validator::isEmail('abc.efg@rad-dev.org', null, array('deep' => true)));
$this->assertFalse(Validator::isEmail('abc.efg@invalidfoo.com', null, array(
'deep' => true
@@ -848,7 +903,7 @@ class ValidatorTest extends \lithium\test\Unit {
public function testCheckHasErrors() {
$rules = array('title' => array('please enter a title'));
- $result = Validator::check(null, $rules);
+ $result = Validator::check(array(), $rules);
$this->assertFalse(empty($result));
$expected = array('title' => array('please enter a title'));
@@ -870,7 +925,7 @@ class ValidatorTest extends \lithium\test\Unit {
array('email', 'message' => 'email is not valid')
)
);
- $result = Validator::check(null, $rules);
+ $result = Validator::check(array(), $rules);
$this->assertFalse(empty($result));
$expected = array(
@@ -890,13 +945,14 @@ class ValidatorTest extends \lithium\test\Unit {
);
$data = array('email' => 'something');
$result = Validator::check($data, $rules);
- $this->assertFalse(empty($result));
- $expected = array(
+ // result:
+ $errors = array(
'title' => array('please enter a title'),
'email' => array('email is not valid')
);
- $this->assertEqual($expected, $result);
+ $this->assertFalse(empty($result));
+ $this->assertEqual($errors, $result);
}
public function testCheckMultipleHasOneError() {
@@ -960,6 +1016,59 @@ class ValidatorTest extends \lithium\test\Unit {
$this->assertTrue(Validator::isInRange(0));
}
+
+ public function testValidationWithContextData() {
+ Validator::add('someModelRule', function($value, $format, $options) {
+ return $value == 'Title' && $options['values']['body'] == 'Body';
+ });
+
+ $result = Validator::check(
+ array('title' => 'Title', 'body' => 'Body'),
+ array('title' => array('someModelRule'))
+ );
+ $this->assertIdentical(array(), $result);
+
+ $result = Validator::check(
+ array('title' => 'Title', 'body' => 'Not Body'),
+ array('title' => array('someModelRule'))
+ );
+ $this->assertIdentical(array('title' => array(0)), $result);
+ }
+
+ /**
+ * Tests that event flags applied to rules only trigger when the corresponding event is passed
+ * in the `$options` parameter of `check()`.
+ *
+ * @return void
+ */
+ public function testEvents() {
+ $rules = array('number' => array('numeric', 'message' => 'Badness!'));
+ $expected = array('number' => array('Badness!'));
+
+ $result = Validator::check(array('number' => 'o'), $rules);
+ $this->assertEqual($expected, $result);
+
+ $rules['number']['on'] = 'foo';
+ $result = Validator::check(array('number' => 'o'), $rules, array('events' => 'foo'));
+ $this->assertEqual($expected, $result);
+
+ $result = Validator::check(array('number' => 'o'), $rules, array('events' => 'bar'));
+ $this->assertEqual(array(), $result);
+
+ $result = Validator::check(array('number' => 'o'), $rules, array(
+ 'events' => array('foo', 'bar')
+ ));
+ $this->assertEqual($expected, $result);
+
+ $result = Validator::check(array('number' => 'o'), $rules, array(
+ 'events' => array('bar', 'baz')
+ ));
+ $this->assertEqual(array(), $result);
+
+ unset($rules['number']['on']);
+ $result = Validator::check(array('number' => 'o'), $rules, array('events' => 'foo'));
+ $this->assertEqual($expected, $result);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/collection/FiltersTest.php b/libraries/lithium/tests/cases/util/collection/FiltersTest.php
index cba8d64..1dfa6f7 100644
--- a/libraries/lithium/tests/cases/util/collection/FiltersTest.php
+++ b/libraries/lithium/tests/cases/util/collection/FiltersTest.php
@@ -2,17 +2,19 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\cases\util\collection;
-use \lithium\util\collection\Filters;
+use lithium\util\collection\Filters;
+use lithium\tests\mocks\util\MockFilters;
class FiltersTest extends \lithium\test\Unit {
+
public function testRun() {
- $options = array('method' => __FUNCTION__, 'class' => __CLASS__, 'items' => array(
+ $options = array('method' => __FUNCTION__, 'class' => __CLASS__, 'data' => array(
function($self, $params, $chain) {
$params['message'] .= 'is a filter chain ';
return $chain->next($self, $params, $chain);
@@ -32,7 +34,7 @@ class FiltersTest extends \lithium\test\Unit {
}
public function testRunWithoutChain() {
- $options = array('method' => __FUNCTION__, 'class' => __CLASS__, 'items' => array(
+ $options = array('method' => __FUNCTION__, 'class' => __CLASS__, 'data' => array(
function($self, $params, $chain) {
return $chain->next($self, $params, null);
},
@@ -42,6 +44,26 @@ class FiltersTest extends \lithium\test\Unit {
$expected = 'This is a filter chain that calls $chain->next() without the $chain argument.';
$this->assertEqual($expected, $result);
}
+
+ public function testLazyApply() {
+ $class = 'lithium\tests\mocks\util\MockFilters';
+
+ Filters::apply($class, 'filteredMethod', function($self, $params, $chain) {
+ return md5($chain->next($self, $params, $chain));
+ });
+
+ $expected = md5('Working?');
+ $result = $class::filteredMethod();
+ $this->assertEqual($expected, $result);
+
+ Filters::apply($class, 'filteredMethod', function($self, $params, $chain) {
+ return sha1($chain->next($self, $params, $chain));
+ });
+
+ $expected = md5(sha1('Working?'));
+ $result = $class::filteredMethod();
+ $this->assertEqual($expected, $result);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/analysis/LoggerTest.php b/libraries/lithium/tests/integration/analysis/LoggerTest.php
new file mode 100644
index 0000000..2394d3e
--- /dev/null
+++ b/libraries/lithium/tests/integration/analysis/LoggerTest.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\analysis;
+
+use lithium\core\Libraries;
+use lithium\analysis\Logger;
+use lithium\util\Collection;
+use lithium\util\collection\Filters;
+
+/**
+ * Logger adapter integration test cases
+ */
+class LoggerTest extends \lithium\test\Unit {
+
+ public function testWriteFilter() {
+
+ $base = Libraries::get(true, 'resources') . '/tmp/logs';
+ $this->skipIf(!is_writable($base), "{$base} is not writable.");
+
+ Filters::apply('lithium\analysis\Logger', 'write', function($self, $params, $chain) {
+ $params['message'] = 'Filtered Message';
+ return $chain->next($self, $params, $chain);
+ });
+
+ $config = array('default' => array('adapter' => 'File', 'timestamp' => false));
+ Logger::config($config);
+
+ $result = Logger::write('info', 'Original Message');
+ $this->assertTrue(file_exists($base . '/info.log'));
+
+ $expected = "Filtered Message\n";
+ $result = file_get_contents($base . '/info.log');
+ $this->assertEqual($expected, $result);
+
+ unlink($base . '/info.log');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/data/CrudTest.php b/libraries/lithium/tests/integration/data/CrudTest.php
new file mode 100644
index 0000000..87eb2cd
--- /dev/null
+++ b/libraries/lithium/tests/integration/data/CrudTest.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\data;
+
+use Exception;
+use lithium\data\Connections;
+use lithium\tests\mocks\data\Companies;
+
+class CrudTest extends \lithium\test\Integration {
+
+ protected $_connection = null;
+
+ protected $_key = null;
+
+ public $companyData = array(
+ array('name' => 'StuffMart', 'active' => true),
+ array('name' => 'Ma \'n Pa\'s Data Warehousing & Bait Shop', 'active' => false)
+ );
+
+ public function setUp() {
+ Companies::config();
+ $this->_key = Companies::key();
+ $this->_connection = Connections::get('test');
+ }
+
+ /**
+ * Skip the test if no test database connection available.
+ *
+ * @return void
+ */
+ public function skip() {
+ $isAvailable = (
+ Connections::get('test', array('config' => true)) &&
+ Connections::get('test')->isConnected(array('autoConnect' => true))
+ );
+ $this->skipIf(!$isAvailable, "No test connection available.");
+ }
+
+ /**
+ * Tests that a single record with a manually specified primary key can be created, persisted
+ * to an arbitrary data store, re-read and updated.
+ *
+ * @return void
+ */
+ public function testCreate() {
+ Companies::all()->delete();
+ $this->assertIdentical(0, Companies::count());
+
+ $new = Companies::create(array('name' => 'Acme, Inc.', 'active' => true));
+ $this->assertEqual($new->data(), array('name' => 'Acme, Inc.', 'active' => true));
+
+ $this->assertEqual(
+ array(false, true, true),
+ array($new->exists(), $new->save(), $new->exists())
+ );
+ $this->assertIdentical(1, Companies::count());
+ }
+
+ public function testRead() {
+ $existing = Companies::first();
+
+ foreach (Companies::key($existing) as $val) {
+ $this->assertTrue($val);
+ }
+ $this->assertEqual('Acme, Inc.', $existing->name);
+ $this->assertTrue($existing->active);
+ $this->assertTrue($existing->exists());
+ }
+
+ public function testUpdate() {
+ $existing = Companies::first();
+ $this->assertEqual($existing->name, 'Acme, Inc.');
+ $existing->name = 'Big Brother and the Holding Company';
+ $result = $existing->save();
+ $this->assertTrue($result);
+
+ $existing = Companies::first();
+ foreach (Companies::key($existing) as $val) {
+ $this->assertTrue($val);
+ }
+ $this->assertTrue($existing->active);
+ $this->assertEqual('Big Brother and the Holding Company', $existing->name);
+ }
+
+ public function testDelete() {
+ $existing = Companies::first();
+ $this->assertTrue($existing->exists());
+ $this->assertTrue($existing->delete());
+ $this->assertNull(Companies::first(array('conditions' => Companies::key($existing))));
+ $this->assertIdentical(0, Companies::count());
+ }
+
+ public function testCrudMulti() {
+ $large = Companies::create(array('name' => 'BigBoxMart', 'active' => true));
+ $medium = Companies::create(array('name' => 'Acme, Inc.', 'active' => true));
+ $small = Companies::create(array('name' => 'Ma & Pa\'s', 'active' => true));
+
+ foreach (array('large', 'medium', 'small') as $key) {
+ $this->assertFalse(${$key}->exists());
+ $this->assertTrue(${$key}->save());
+ $this->assertTrue(${$key}->exists());
+ }
+ $this->assertEqual(3, Companies::count());
+
+ $all = Companies::all();
+ $this->assertEqual(3, $all->count());
+
+ $match = 'BigBoxMart';
+ $filter = function($entity) use (&$match) { return $entity->name == $match; };
+
+ foreach (array('BigBoxMart', 'Acme, Inc.', 'Ma & Pa\'s') as $match) {
+ $this->assertTrue($all->first($filter)->exists());
+ }
+ $this->assertEqual(array(true, true, true), array_values($all->delete()));
+ $this->assertEqual(0, Companies::count());
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/data/DatabaseTest.php b/libraries/lithium/tests/integration/data/DatabaseTest.php
new file mode 100644
index 0000000..8cfe62d
--- /dev/null
+++ b/libraries/lithium/tests/integration/data/DatabaseTest.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\data;
+
+use lithium\data\Connections;
+use lithium\data\model\Query;
+use lithium\data\source\Database;
+
+class DatabaseTest extends \lithium\test\Unit {
+
+ public $db = null;
+
+ public function skip() {
+ $isAvailable = (
+ Connections::get('test', array('config' => true)) &&
+ Connections::get('test')->isConnected(array('autoConnect' => true))
+ );
+ $this->skipIf(!$isAvailable, "No test connection available.");
+
+ $isDatabase = Connections::get('test') instanceof Database;
+ $this->skipIf(!$isAvailable, "The 'test' connection is not a relational database.");
+ }
+
+ public function setUp() {
+ $this->db = Connections::get('test');
+ }
+
+ public function testQueryManyToOne() {
+ $query = new Query(array(
+ 'type' => 'read',
+ 'model' => 'lithium\tests\mocks\data\source\Images',
+ 'source' => 'images',
+ 'alias' => 'Images',
+ 'joins' => array(new Query(array(
+ 'type' => 'LEFT',
+ 'model' => 'lithium\tests\mocks\data\source\Galleries',
+ 'source' => 'galleries',
+ 'alias' => 'Gallery',
+ 'constraint' => array('Gallery.id' => 'Images.gallery_id')
+ )))
+ ));
+ $images = $this->db->read($query);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/data/FieldsTest.php b/libraries/lithium/tests/integration/data/FieldsTest.php
new file mode 100644
index 0000000..fb0072d
--- /dev/null
+++ b/libraries/lithium/tests/integration/data/FieldsTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace lithium\tests\integration\data;
+
+use lithium\data\Connections;
+use lithium\data\Entity;
+use lithium\tests\mocks\data\Company;
+
+class FieldsTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Company::config();
+ }
+
+ public function tearDown() {
+ Company::remove();
+ }
+
+ public function skip() {
+ $isAvailable = (
+ Connections::get('test', array('config' => true)) &&
+ Connections::get('test')->isConnected(array('autoConnect' => true))
+ );
+ $this->skipIf(!$isAvailable, "No test connection available");
+ }
+
+ public function testSingleField() {
+ $new = Company::create(array('name' => 'Acme, Inc.'));
+ $key = Company::meta('key');
+ $new->save();
+ $id = is_object($new->{$key}) ? (string) $new->{$key} : $new->{$key};
+
+ $entity = Company::first($id);
+
+ $this->assertTrue($entity instanceof Entity);
+ $this->skipIf(!$entity instanceof Entity, 'Queried object is not an entity.');
+
+ $expected = array($key => $id, 'name' => 'Acme, Inc.');
+ $result = $entity->data();
+ $this->assertEqual($expected, $result);
+
+ $entity = Company::first(array('fields' => array($key)));
+
+ $this->assertTrue($entity instanceof Entity);
+ $this->skipIf(!$entity instanceof Entity, 'Queried object is not an entity.');
+
+ $expected = array($key => $id);
+ $result = $entity->data();
+ $this->assertEqual($expected, $result);
+
+ $entity = Company::find('first',array(
+ 'conditions' => array($key => $id),
+ 'fields' => array($key)
+ ));
+ $this->assertTrue($entity instanceof Entity);
+ $this->skipIf(!$entity instanceof Entity, 'Queried object is not an entity.');
+
+ $result = $entity->save();
+ $this->assertTrue($result);
+ $new->delete();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/data/SourceTest.php b/libraries/lithium/tests/integration/data/SourceTest.php
index db7a890..c356918 100644
--- a/libraries/lithium/tests/integration/data/SourceTest.php
+++ b/libraries/lithium/tests/integration/data/SourceTest.php
@@ -2,41 +2,65 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\integration\data;
-use \Exception;
-use \ArrayAccess;
-use \lithium\data\Connections;
+use Exception;
+use lithium\data\Connections;
+use lithium\tests\mocks\data\Company;
+use lithium\tests\mocks\data\Employee;
-class Company extends \lithium\data\Model {
+class SourceTest extends \lithium\test\Unit {
- protected $_meta = array(
- 'connection' => 'test',
- 'source' => 'companies'
- );
-}
+ protected $_connection = null;
-class SourceTest extends \lithium\test\Unit {
+ protected $_classes = array(
+ 'employee' => 'lithium\tests\mocks\data\Employee',
+ 'company' => 'lithium\tests\mocks\data\Company'
+ );
public $companyData = array(
- array('name' => 'BigBoxMart'),
- array('name' => 'Ma \'n Pa\'s Data Warehousing & Bait Shop')
+ array('name' => 'StuffMart', 'active' => true),
+ array('name' => 'Ma \'n Pa\'s Data Warehousing & Bait Shop', 'active' => false)
);
+ /**
+ * @todo Make less dumb.
+ *
+ */
public function setUp() {
- Company::__init();
+ Company::config();
+ Employee::config();
+ $this->_connection = Connections::get('test');
+
+ if (strpos(get_class($this->_connection), 'CouchDb')) {
+ $this->_loadViews();
+ }
+
+ try {
+ foreach (Company::all() as $company) {
+ $company->delete();
+ }
+ } catch (Exception $e) {}
+ }
+
+ protected function _loadViews() {
+ Company::create()->save();
}
+ /**
+ * @todo Make this less dumb.
+ */
public function tearDown() {
try {
foreach (Company::all() as $company) {
$company->delete();
}
} catch (Exception $e) {
+ $this->assertTrue(false, $e->getMessage());
}
}
@@ -50,7 +74,7 @@ class SourceTest extends \lithium\test\Unit {
Connections::get('test', array('config' => true)) &&
Connections::get('test')->isConnected(array('autoConnect' => true))
);
- $this->skipIf(!$isAvailable, "No test connection available");
+ $this->skipIf(!$isAvailable, "No test connection available.");
}
/**
@@ -61,7 +85,6 @@ class SourceTest extends \lithium\test\Unit {
*/
public function testSingleReadWriteWithKey() {
$key = Company::meta('key');
-
$new = Company::create(array($key => 12345, 'name' => 'Acme, Inc.'));
$result = $new->data();
@@ -80,7 +103,8 @@ class SourceTest extends \lithium\test\Unit {
$this->assertTrue($existing->exists());
$existing->name = 'Big Brother and the Holding Company';
- $this->assertTrue($existing->save());
+ $result = $existing->save();
+ $this->assertTrue($result);
$existing = Company::find(12345);
$result = $existing->data();
@@ -88,7 +112,47 @@ class SourceTest extends \lithium\test\Unit {
$this->assertEqual($expected[$key], $result[$key]);
$this->assertEqual($expected['name'], $result['name']);
- $existing->delete();
+ $this->assertTrue($existing->delete());
+ }
+
+ public function testRewind() {
+ $key = Company::meta('key');
+ $new = Company::create(array($key => 12345, 'name' => 'Acme, Inc.'));
+
+ $result = $new->data();
+ $this->assertTrue($result !== null);
+ $this->assertTrue($new->save());
+ $this->assertTrue($new->exists());
+
+ $result = Company::all(12345);
+ $this->assertTrue($result !== null);
+
+ $result = $result->rewind();
+ $this->assertTrue($result !== null);
+ $this->assertTrue(!is_string($result));
+ }
+
+ public function testFindFirstWithFieldsOption() {
+ return;
+ $key = Company::meta('key');
+ $new = Company::create(array($key => 1111, 'name' => 'Test find first with fields.'));
+ $result = $new->data();
+
+ $expected = array($key => 1111, 'name' => 'Test find first with fields.');
+ $this->assertEqual($expected['name'], $result['name']);
+ $this->assertEqual($expected[$key], $result[$key]);
+ $this->assertFalse($new->exists());
+ $this->assertTrue($new->save());
+ $this->assertTrue($new->exists());
+
+ $result = Company::find('first', array('fields' => array('name')));
+ $this->assertFalse(is_null($result));
+
+ $this->skipIf(is_null($result), 'No result returned to test');
+ $result = $result->data();
+ $this->assertEqual($expected['name'], $result['name']);
+
+ $this->assertTrue($new->delete());
}
public function testReadWriteMultiple() {
@@ -97,11 +161,16 @@ class SourceTest extends \lithium\test\Unit {
foreach ($this->companyData as $data) {
$companies[] = Company::create($data);
- $this->assertTrue($companies[count($companies) - 1]->save());
- $this->assertTrue($companies[count($companies) - 1]->{$key});
+ $this->assertTrue(end($companies)->save());
+ $this->assertTrue(end($companies)->{$key});
}
+ $this->assertIdentical(2, Company::count());
+ $this->assertIdentical(1, Company::count(array('active' => true)));
+ $this->assertIdentical(1, Company::count(array('active' => false)));
+ $this->assertIdentical(0, Company::count(array('active' => null)));
$all = Company::all();
+ $this->assertIdentical(2, Company::count());
$expected = count($this->companyData);
$this->assertEqual($expected, $all->count());
@@ -109,36 +178,33 @@ class SourceTest extends \lithium\test\Unit {
$id = (string) $all->first()->{$key};
$this->assertTrue(strlen($id) > 0);
- $this->assertTrue($all->data());
+ $this->assertTrue($all->data());
foreach ($companies as $company) {
$this->assertTrue($company->delete());
}
+ $this->assertIdentical(0, Company::count());
}
- public function testRecordOffset() {
+ public function testEntityFields() {
foreach ($this->companyData as $data) {
Company::create($data)->save();
}
$all = Company::all();
- $result = $all->first();
- $this->skipIf(!$result instanceof ArrayAccess, 'Data class does not implement ArrayAccess');
-
- $expected = 'BigBoxMart';
- $this->assertEqual($expected, $result['name']);
+ $result = $all->first(function($doc) { return $doc->name == 'StuffMart'; });
+ $this->assertEqual('StuffMart', $result->name);
$result = $result->data();
- $this->assertEqual($expected, $result['name']);
+ $this->assertEqual('StuffMart', $result['name']);
- $result = $all[1];
- $expected = 'Ma \'n Pa\'s Data Warehousing & Bait Shop';
- $this->assertEqual($expected, $result['name']);
+ $result = $all->next();
+ $this->assertEqual('Ma \'n Pa\'s Data Warehousing & Bait Shop', $result->name);
$result = $result->data();
- $this->assertEqual($expected, $result['name']);
+ $this->assertEqual('Ma \'n Pa\'s Data Warehousing & Bait Shop', $result['name']);
- $this->assertNull($all[2]);
+ $this->assertNull($all->next());
}
/**
@@ -153,9 +219,93 @@ class SourceTest extends \lithium\test\Unit {
$company = Company::create(array('name' => 'Test Company'));
$this->assertTrue($company->save());
- $id = $company->{$key};
- $companyCopy = Company::find($id);
- $this->assertEqual($company->data(), $companyCopy->data());
+ $id = (string) $company->{$key};
+ $companyCopy = Company::find($id)->data();
+ $data = $company->data();
+
+ foreach ($data as $key => $value) {
+ $this->assertTrue(isset($companyCopy[$key]));
+ $this->assertEqual($data[$key], $companyCopy[$key]);
+ }
+ }
+
+ /**
+ * Tests the default relationship information provided by the backend data source.
+ *
+ * @return void
+ */
+ public function testDefaultRelationshipInfo() {
+ $connection = $this->_connection;
+ $message = "Relationships are not supported by this adapter.";
+ $this->skipIf(!$connection::enabled('relationships'), $message);
+
+ $this->assertEqual(array('Employees'), array_keys(Company::relations()));
+ $this->assertEqual(array('Company'), array_keys(Employee::relations()));
+
+ $this->assertEqual(array('Employees'), Company::relations('hasMany'));
+ $this->assertEqual(array('Company'), Employee::relations('belongsTo'));
+
+ $this->assertFalse(Company::relations('belongsTo'));
+ $this->assertFalse(Company::relations('hasOne'));
+
+ $this->assertFalse(Employee::relations('hasMany'));
+ $this->assertFalse(Employee::relations('hasOne'));
+
+ $result = Company::relations('Employees');
+
+ $this->assertEqual('hasMany', $result->data('type'));
+ $this->assertEqual($this->_classes['employee'], $result->data('to'));
+ }
+
+ public function testRelationshipQuerying() {
+ $connection = $this->_connection;
+ $message = "Relationships are not supported by this adapter.";
+ $this->skipIf(!$connection::enabled('relationships'), $message);
+
+ foreach ($this->companyData as $data) {
+ Company::create($data)->save();
+ }
+ $stuffMart = Company::findFirstByName('StuffMart');
+ $maAndPas = Company::findFirstByName('Ma \'n Pa\'s Data Warehousing & Bait Shop');
+
+ $this->assertEqual($this->_classes['employee'], $stuffMart->employees->model());
+ $this->assertEqual($this->_classes['employee'], $maAndPas->employees->model());
+
+ foreach (array('Mr. Smith', 'Mr. Jones', 'Mr. Brown') as $name) {
+ $stuffMart->employees[] = Employee::create(compact('name'));
+ }
+ $expected = Company::key($stuffMart) + array(
+ 'name' => 'StuffMart', 'active' => true, 'employees' => array(
+ array('name' => 'Mr. Smith'),
+ array('name' => 'Mr. Jones'),
+ array('name' => 'Mr. Brown')
+ )
+ );
+ $this->assertEqual($expected, $stuffMart->data());
+ $this->assertTrue($stuffMart->save());
+ $this->assertEqual('Smith', $stuffMart->employees[0]->lastName());
+
+ $stuffMartReloaded = Company::findFirstByName('StuffMart');
+ $this->assertEqual('Smith', $stuffMartReloaded->employees[0]->lastName());
+
+ foreach (array('Ma', 'Pa') as $name) {
+ $maAndPas->employees[] = Employee::create(compact('name'));
+ }
+ $maAndPas->save();
+ }
+
+ public function testAbstractTypeHandling() {
+ $key = Company::meta('key');
+
+ foreach ($this->companyData as $data) {
+ $companies[] = Company::create($data);
+ $this->assertTrue(end($companies)->save());
+ $this->assertTrue(end($companies)->{$key});
+ }
+
+ foreach (Company::all() as $company) {
+ $this->assertTrue($company->delete());
+ }
}
}
diff --git a/libraries/lithium/tests/integration/data/source/CouchDbTest.php b/libraries/lithium/tests/integration/data/source/CouchDbTest.php
new file mode 100644
index 0000000..e7ed518
--- /dev/null
+++ b/libraries/lithium/tests/integration/data/source/CouchDbTest.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\data\source;
+
+use lithium\data\Connections;
+use lithium\tests\mocks\MockCouchModel;
+
+class CouchDbTest extends \lithium\test\Integration {
+
+ public function setUp() {
+ MockCouchModel::meta(array('connection' => 'test'));
+ }
+
+ public function tearDown() {
+ $results = MockCouchModel::all();
+ if ($results->count()) {
+ $results->delete();
+ }
+ }
+
+ /**
+ * Skip the test if no `test` CouchDb connection available.
+ *
+ * @return void
+ */
+ public function skip() {
+ $isAvailable = (
+ Connections::get('test', array('config' => true)) &&
+ Connections::get('test')->isConnected(array('autoConnect' => true))
+ );
+ $this->skipIf(!$isAvailable, "No test connection available");
+
+ $couchConnection = strpos(get_class(Connections::get('test')), 'CouchDb');
+ $this->skipIf(!$couchConnection, "Test connection is not CouchDb");
+ }
+
+ public function testCreate() {
+ $result = MockCouchModel::create()->save();
+ $this->assertTrue($result);
+ }
+
+ public function testSavingIdWithLockedModel() {
+ $id = 'myCustomId';
+ $model = MockCouchModel::create(compact('id'));
+ $result = $model->save();
+
+ $this->assertTrue($result);
+
+ $data = $model->data();
+ $expected = $id;
+ $this->assertNotEqual($expected, $data['id']);
+ }
+
+ public function testSavingIdWithUnlockedModel() {
+ MockCouchModel::meta(array('locked' => false));
+ $id = 'myCustomId';
+ $model = MockCouchModel::create(compact('id'));
+ $result = $model->save();
+ $this->assertTrue($result);
+
+ $data = $model->data();
+ $expected = $id;
+ $this->assertEqual($expected, $data['id']);
+ }
+
+ public function testUpdate() {
+ $model = MockCouchModel::create(array('someKey' => 'someValue'));
+ $result = $model->save();
+ $this->assertTrue($result);
+
+ $data = $model->data();
+
+ $this->assertTrue(array_key_exists('id', $data));
+ $this->assertTrue(array_key_exists('rev', $data));
+
+ $expected = 'someValue';
+ $this->assertEqual($expected, $data['someKey']);
+
+ $model->someKey = 'someOtherValue';
+ $result = $model->save();
+ $this->assertTrue($result);
+
+ $updated = $model->data();
+
+ $expected = 'someOtherValue';
+ $this->assertEqual($expected, $updated['someKey']);
+ $this->assertEqual($data['id'], $updated['id']);
+ $this->assertNotEqual($data['rev'], $updated['rev']);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/g11n/CatalogInflectorTest.php b/libraries/lithium/tests/integration/g11n/CatalogInflectorTest.php
index 6373cdb..6c1226d 100644
--- a/libraries/lithium/tests/integration/g11n/CatalogInflectorTest.php
+++ b/libraries/lithium/tests/integration/g11n/CatalogInflectorTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\integration\g11n;
-use \lithium\g11n\Catalog;
-use \lithium\g11n\catalog\adapter\Memory;
-use \lithium\util\Inflector;
+use lithium\g11n\Catalog;
+use lithium\g11n\catalog\adapter\Memory;
+use lithium\util\Inflector;
class CatalogInflectorTest extends \lithium\test\Unit {
@@ -37,9 +37,11 @@ class CatalogInflectorTest extends \lithium\test\Unit {
'&' => 'and'
)
);
- Catalog::write('inflection', 'en', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'inflection', 'en', $data);
- Inflector::rules('transliteration', Catalog::read('inflection.transliteration', 'en'));
+ Inflector::rules(
+ 'transliteration', Catalog::read('runtime', 'inflection.transliteration', 'en')
+ );
$result = Inflector::slug('this & that');
$expected = 'this-and-that';
@@ -51,9 +53,11 @@ class CatalogInflectorTest extends \lithium\test\Unit {
'&' => 'und'
)
);
- Catalog::write('inflection', 'de', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'inflection', 'de', $data);
- Inflector::rules('transliteration', Catalog::read('inflection.transliteration', 'de'));
+ Inflector::rules(
+ 'transliteration', Catalog::read('runtime', 'inflection.transliteration', 'de')
+ );
$result = Inflector::slug('this & that');
$expected = 'dhis-und-dhad';
diff --git a/libraries/lithium/tests/integration/g11n/CatalogValidatorTest.php b/libraries/lithium/tests/integration/g11n/CatalogValidatorTest.php
index 8d18803..b871bf5 100644
--- a/libraries/lithium/tests/integration/g11n/CatalogValidatorTest.php
+++ b/libraries/lithium/tests/integration/g11n/CatalogValidatorTest.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\integration\g11n;
-use \lithium\g11n\Catalog;
-use \lithium\g11n\catalog\adapter\Memory;
-use \lithium\util\Validator;
+use lithium\g11n\Catalog;
+use lithium\g11n\catalog\adapter\Memory;
+use lithium\util\Validator;
class CatalogValidatorTest extends \lithium\test\Unit {
@@ -32,9 +32,9 @@ class CatalogValidatorTest extends \lithium\test\Unit {
public function testFlat() {
$data = '/postalCode en_US/';
- Catalog::write('validation.postalCode', 'en_US', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'validation.postalCode', 'en_US', $data);
- Validator::add('postalCode', Catalog::read('validation.postalCode', 'en_US'));
+ Validator::add('postalCode', Catalog::read('runtime', 'validation.postalCode', 'en_US'));
$result = Validator::isPostalCode('postalCode en_US');
$this->assertTrue($result);
@@ -45,9 +45,9 @@ class CatalogValidatorTest extends \lithium\test\Unit {
'postalCode' => '/postalCode en_US/',
'phone' => '/phone en_US/',
);
- Catalog::write('validation', 'en_US', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'validation', 'en_US', $data);
- Validator::add(Catalog::read('validation', 'en_US'));
+ Validator::add(Catalog::read('runtime', 'validation', 'en_US'));
$result = Validator::isPostalCode('postalCode en_US');
$this->assertTrue($result);
@@ -58,13 +58,13 @@ class CatalogValidatorTest extends \lithium\test\Unit {
public function testMultipleLocales() {
$data = '/phone en_US/';
- Catalog::write('validation.phone', 'en_US', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'validation.phone', 'en_US', $data);
$data = '/phone en_GB/';
- Catalog::write('validation.phone', 'en_GB', $data, array('name' => 'runtime'));
+ Catalog::write('runtime', 'validation.phone', 'en_GB', $data);
Validator::add('phone', array(
- 'en_US' => Catalog::read('validation.phone', 'en_US'),
- 'en_GB' => Catalog::Read('validation.phone', 'en_GB')
+ 'en_US' => Catalog::read('runtime', 'validation.phone', 'en_US'),
+ 'en_GB' => Catalog::read('runtime', 'validation.phone', 'en_GB')
));
$result = Validator::isPhone('phone en_US', 'en_US');
diff --git a/libraries/lithium/tests/integration/g11n/ResourcesMessageTest.php b/libraries/lithium/tests/integration/g11n/ResourcesMessageTest.php
new file mode 100644
index 0000000..526dd0a
--- /dev/null
+++ b/libraries/lithium/tests/integration/g11n/ResourcesMessageTest.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\g11n;
+
+use lithium\g11n\Catalog;
+use lithium\util\Validator;
+
+/**
+ * Test for integration of g11n resources. Numbers of rules refer to those documented in
+ * the document on pluralization at Mozilla.
+ *
+ * @link https://developer.mozilla.org/en/Localization_and_Plurals
+ */
+class ResourcesMessageTest extends \lithium\test\Unit {
+
+ protected $_backups = array();
+
+ public function setUp() {
+ $this->_backups['catalogConfig'] = Catalog::config();
+ Catalog::reset();
+ Catalog::config(array(
+ 'lithium' => array(
+ 'adapter' => 'Php',
+ 'path' => LITHIUM_LIBRARY_PATH . '/lithium/g11n/resources/php'
+ )
+ ));
+ }
+
+ public function tearDown() {
+ Catalog::reset();
+ Catalog::config($this->_backups['catalogConfig']);
+ }
+
+ /**
+ * Tests the plural rule #1 which applies to the following languages
+ * grouped by family and sorted alphabetically.
+ *
+ * Germanic family:
+ * - English (en)
+ *
+ * @return void
+ */
+ public function testPlurals1() {
+ $locales = array('en');
+
+ foreach ($locales as $locale) {
+ $result = Catalog::read('lithium', 'message.pluralForms', $locale);
+ $this->assertEqual(2, $result, "Locale: `{$locale}`\n{:message}");
+
+ $rule = Catalog::read('lithium', 'message.pluralRule', $locale);
+
+ $expected = '10111111111111111111111111111111111111111111111111';
+ $expected .= '11111111111111111111111111111111111111111111111111';
+ $expected .= '11111111111111111111111111111111111111111111111111';
+ $expected .= '11111111111111111111111111111111111111111111111111';
+ $result = '';
+
+ for ($n = 0; $n < 200; $n++) {
+ $result .= $rule($n);
+ }
+ $this->assertIdentical($expected, $result, "Locale: `{$locale}`\n{:message}");
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/g11n/ResourcesValidatorTest.php b/libraries/lithium/tests/integration/g11n/ResourcesValidatorTest.php
index e650b2a..abd8dbc 100644
--- a/libraries/lithium/tests/integration/g11n/ResourcesValidatorTest.php
+++ b/libraries/lithium/tests/integration/g11n/ResourcesValidatorTest.php
@@ -2,15 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\integration\g11n;
-use \lithium\g11n\Catalog;
-use \lithium\g11n\catalog\adapter\Memory;
-use \lithium\util\Validator;
+use lithium\g11n\Catalog;
+use lithium\util\Validator;
class ResourcesValidatorTest extends \lithium\test\Unit {
@@ -33,14 +32,14 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testDaDk() {
- Validator::add(Catalog::read('validation', 'da_DK'));
+ Validator::add(Catalog::read('lithium', 'validation', 'da_DK'));
$this->assertTrue(Validator::isSsn('123456-1234'));
$this->assertFalse(Validator::isSsn('12345-1234'));
}
public function testDeBe() {
- Validator::add(Catalog::read('validation', 'de_BE'));
+ Validator::add(Catalog::read('lithium', 'validation', 'de_BE'));
$this->assertTrue(Validator::isPostalCode('1234'));
$this->assertTrue(Validator::isPostalCode('1234'));
@@ -48,14 +47,14 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testDeDe() {
- Validator::add(Catalog::read('validation', 'de_DE'));
+ Validator::add(Catalog::read('lithium', 'validation', 'de_DE'));
$this->assertTrue(Validator::isPostalCode('12345'));
$this->assertFalse(Validator::isPostalCode('123456'));
}
public function testEnCa() {
- Validator::add(Catalog::read('validation', 'en_CA'));
+ Validator::add(Catalog::read('lithium', 'validation', 'en_CA'));
$this->assertTrue(Validator::isPhone('(401) 321-9876'));
@@ -64,7 +63,7 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testEnGb() {
- Validator::add(Catalog::read('validation', 'en_GB'));
+ Validator::add(Catalog::read('lithium', 'validation', 'en_GB'));
$this->assertTrue(Validator::isPostalCode('M1 1AA'));
$this->assertTrue(Validator::isPostalCode('M60 1NW'));
@@ -82,7 +81,7 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testEnUs() {
- Validator::add(Catalog::read('validation', 'en_US'));
+ Validator::add(Catalog::read('lithium', 'validation', 'en_US'));
$this->assertTrue(Validator::isPhone('(401) 321-9876'));
@@ -95,7 +94,7 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testFrBe() {
- Validator::add(Catalog::read('validation', 'fr_BE'));
+ Validator::add(Catalog::read('lithium', 'validation', 'fr_BE'));
$this->assertTrue(Validator::isPostalCode('1234'));
$this->assertTrue(Validator::isPostalCode('1234'));
@@ -103,7 +102,7 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testFrCa() {
- Validator::add(Catalog::read('validation', 'fr_CA'));
+ Validator::add(Catalog::read('lithium', 'validation', 'fr_CA'));
$this->assertTrue(Validator::isPhone('(401) 321-9876'));
@@ -112,14 +111,14 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testItIt() {
- Validator::add(Catalog::read('validation', 'it_IT'));
+ Validator::add(Catalog::read('lithium', 'validation', 'it_IT'));
$this->assertTrue(Validator::isPostalCode('12345'));
$this->assertFalse(Validator::isPostalCode('123456'));
}
public function testNlBe() {
- Validator::add(Catalog::read('validation', 'nl_BE'));
+ Validator::add(Catalog::read('lithium', 'validation', 'nl_BE'));
$this->assertTrue(Validator::isPostalCode('1234'));
$this->assertTrue(Validator::isPostalCode('1234'));
@@ -127,7 +126,7 @@ class ResourcesValidatorTest extends \lithium\test\Unit {
}
public function testNlNl() {
- Validator::add(Catalog::read('validation', 'nl_NL'));
+ Validator::add(Catalog::read('lithium', 'validation', 'nl_NL'));
$this->assertTrue(Validator::isSsn('123456789'));
$this->assertFalse(Validator::isSsn('12345678'));
diff --git a/libraries/lithium/tests/integration/net/SocketTest.php b/libraries/lithium/tests/integration/net/SocketTest.php
new file mode 100644
index 0000000..32a6c16
--- /dev/null
+++ b/libraries/lithium/tests/integration/net/SocketTest.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\net;
+
+use lithium\net\socket\Context;
+use lithium\net\socket\Curl;
+use lithium\net\socket\Stream;
+
+class SocketTest extends \lithium\test\Integration {
+
+ protected $_testConfig = array(
+ 'persistent' => false,
+ 'scheme' => 'http',
+ 'host' => 'www.google.com',
+ 'port' => 80,
+ 'timeout' => 1,
+ 'classes' => array(
+ 'request' => 'lithium\net\http\Request',
+ 'response' => 'lithium\net\http\Response'
+ )
+ );
+
+ public function skip() {
+ $config = $this->_testConfig;
+ $message = "Could not open {$config['host']} - skipping " . __CLASS__;
+ $this->skipIf($config['host'] == gethostbyname($config['host']), $message);
+ }
+
+ public function testContextAdapter() {
+ $socket = new Context($this->_testConfig);
+ $this->assertTrue($socket->open());
+ $response = $socket->send();
+ $this->assertTrue($response instanceof \lithium\net\http\Response);
+
+ $expected = 'www.google.com';
+ $result = $response->host;
+ $this->assertEqual($expected, $result);
+
+ $result = $response->body();
+ $this->assertPattern("/<title[^>]*>Google<\/title>/im", (string) $result);
+ }
+
+ public function testCurlAdapter() {
+ $socket = new Curl($this->_testConfig);
+ $this->assertTrue($socket->open());
+ $response = $socket->send();
+ $this->assertTrue($response instanceof \lithium\net\http\Response);
+
+ $expected = 'www.google.com';
+ $result = $response->host;
+ $this->assertEqual($expected, $result);
+
+ $result = $response->body();
+ $this->assertPattern("/<title[^>]*>Google<\/title>/im", (string) $result);
+ }
+
+ public function testStreamAdapter() {
+ $socket = new Stream($this->_testConfig);
+ $this->assertTrue($socket->open());
+ $response = $socket->send();
+ $this->assertTrue($response instanceof \lithium\net\http\Response);
+
+ $expected = 'www.google.com';
+ $result = $response->host;
+ $this->assertEqual($expected, $result);
+
+ $result = $response->body();
+ $this->assertPattern("/<title[^>]*>Google<\/title>/im", (string) $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/net/http/ServiceTest.php b/libraries/lithium/tests/integration/net/http/ServiceTest.php
new file mode 100644
index 0000000..e9405fb
--- /dev/null
+++ b/libraries/lithium/tests/integration/net/http/ServiceTest.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\net\http;
+
+use lithium\net\http\Service;
+
+class ServiceTest extends \lithium\test\Integration {
+
+ public function testStreamGet() {
+ $service = new Service(array(
+ 'classes' => array('socket' => '\lithium\net\socket\Stream')
+ ));
+ $service->head();
+
+ $expected = array('code' => 200, 'message' => 'OK');
+ $result = $service->last->response->status;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testContextGet() {
+ $service = new Service(array(
+ 'classes' => array('socket' => '\lithium\net\socket\Context')
+ ));
+ $service->head();
+
+ $expected = array('code' => 200, 'message' => 'OK');
+ $result = $service->last->response->status;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testCurlGet() {
+ $service = new Service(array(
+ 'classes' => array('socket' => '\lithium\net\socket\Curl')
+ ));
+ $service->head();
+
+ $expected = array('code' => 200, 'message' => 'OK');
+ $result = $service->last->response->status;
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/storage/CookieTest.php b/libraries/lithium/tests/integration/storage/CookieTest.php
new file mode 100644
index 0000000..9278fcd
--- /dev/null
+++ b/libraries/lithium/tests/integration/storage/CookieTest.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\storage;
+
+use lithium\storage\Session;
+
+class CookieTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Session::reset();
+ $cookies = array_keys($_COOKIE);
+
+ foreach ($cookies as $cookie) {
+ setcookie($cookie, "", time()-1);
+ }
+ }
+
+ public function tearDown() {
+ Session::reset();
+ $cookies = array_keys($_COOKIE);
+
+ foreach ($cookies as $cookie) {
+ setcookie($cookie, "", time()-1);
+ }
+ }
+
+ public function testCookieWriteReadDelete() {
+ Session::config(array(
+ 'li3' => array(
+ 'adapter' => 'Cookie',
+ 'expiry' => '+1 day',
+ )
+ ));
+
+ Session::write('ns.testkey1', 'value1', array('name' => 'li3'));
+ Session::write('ns.testkey2', 'value2', array('name' => 'li3'));
+ Session::write('ns.testkey3', 'value3', array('name' => 'li3'));
+
+ $this->assertCookie(
+ array('key' => 'ns.testkey1', 'value' => 'value1')
+ );
+ $this->assertCookie(
+ array('key' => 'ns.testkey2', 'value' => 'value2')
+ );
+ $this->assertCookie(
+ array('key' => 'ns.testkey3', 'value' => 'value3')
+ );
+
+ Session::delete('ns.testkey1', array('name' => 'li3'));
+ Session::delete('ns.testkey2', array('name' => 'li3'));
+ Session::delete('ns.testkey3', array('name' => 'li3'));
+
+ $params = array('exires' => '-1 second', 'path' => '/');
+
+ $this->assertNoCookie(array('key' => 'ns.testkey1'));
+ $this->assertNoCookie(array('key' => 'ns.testkey2'));
+ $this->assertNoCookie(array('key' => 'ns.testkey3'));
+ }
+
+ public function testStrategiesPhpAdapter() {
+ Session::config(array(
+ 'strategy' => array(
+ 'adapter' => 'Php',
+ 'strategies' => array('Hmac' => array('secret' => 'somesecretkey'))
+ )
+ ));
+
+ $key = 'test';
+ $value = 'value';
+
+ Session::write($key, $value, array('name' => 'strategy'));
+ $result = Session::read($key, array('name' => 'strategy'));
+
+ $this->assertEqual($value, $result);
+ $this->assertTrue(Session::delete($key, array('name' => 'strategy')));
+ $result = Session::read($key, array('name' => 'strategy'));
+ $this->assertNull($result);
+
+ Session::write($key, $value, array('name' => 'strategy'));
+ $result = Session::read($key, array('name' => 'strategy'));
+ $this->assertEqual($value, $result);
+
+ $cache = $_SESSION;
+ $_SESSION['injectedkey'] = 'hax0r';
+ $this->expectException('/Possible data tampering - HMAC signature does not match data./');
+ $result = Session::read($key, array('name' => 'strategy'));
+ $_SESSION = $cache;
+ }
+
+ public function testStrategiesCookieAdapter() {
+ $key = 'test_key';
+ $value = 'test_value';
+
+ Session::config(array(
+ 'default' => array(
+ 'adapter' => 'Cookie',
+ 'strategies' => array('Hmac' => array('secret' => 'somesecretkey')),
+ )
+ ));
+
+ $result = Session::write($key, $value);
+ $this->assertTrue($result);
+
+ $result = Session::read($key);
+ $this->assertEqual($value, $result);
+
+ $this->assertTrue(Session::delete($key));
+
+ $result = Session::read($key);
+ $this->assertNull($result);
+
+ Session::write($key, $value);
+ $result = Session::read($key);
+ $this->assertEqual($value, $result);
+ $this->assertTrue(Session::delete($key));
+ }
+
+ public function testHmacStrategy() {
+ $key = 'test';
+ $value = 'value';
+ $name = 'hmac_test';
+
+ Session::config(array(
+ 'default' => array(
+ 'adapter' => 'Cookie',
+ 'strategies' => array('Hmac' => array('secret' => 'somesecretkey')),
+ 'name' => $name
+ )
+ ));
+
+ $cache = $_COOKIE;
+ $_COOKIE[$name]['injectedkey'] = 'hax0r';
+ $this->expectException('/Possible data tampering - HMAC signature does not match data./');
+ $result = Session::read($key, array('name' => 'hmac'));
+ $_COOKIE = $cache;
+ }
+}
+
+?>
diff --git a/libraries/lithium/tests/integration/storage/SessionPhpTest.php b/libraries/lithium/tests/integration/storage/SessionPhpTest.php
deleted file mode 100644
index 84c7e04..0000000
--- a/libraries/lithium/tests/integration/storage/SessionPhpTest.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\integration\storage;
-
-use \lithium\storage\Session;
-
-Session::config(array(
- 'test' => array(
- 'name' => 'test',
- 'adapter' => 'Php',
- 'cookie_lifetime' => 0
- )
-));
-
-class SessionPhpTest extends \lithium\test\Unit {
-
- public function testWriteReadDelete() {
- $key = 'test';
- $value = 'value';
- Session::write($key, $value, array('name' => 'test'));
- $result = Session::read($key, array('name' => 'test'));
- $this->assertEqual($value, $result);
- $this->assertTrue(Session::delete($key, array('name' => 'test')));
- $result = Session::read($key, array('name' => 'test'));
- $this->assertNull($result);
- }
-
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/storage/SessionTest.php b/libraries/lithium/tests/integration/storage/SessionTest.php
new file mode 100644
index 0000000..b2e1e7c
--- /dev/null
+++ b/libraries/lithium/tests/integration/storage/SessionTest.php
@@ -0,0 +1,181 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\storage;
+
+use lithium\storage\Session;
+
+class SessionTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Session::reset();
+ $cookies = array_keys($_COOKIE);
+
+ foreach ($cookies as $cookie) {
+ setcookie($cookie, "", time()-1);
+ }
+ }
+
+ public function tearDown() {
+ Session::reset();
+ $cookies = array_keys($_COOKIE);
+
+ foreach ($cookies as $cookie) {
+ setcookie($cookie, "", time()-1);
+ }
+ }
+
+ public function testWriteAndRead() {
+ Session::config(array(
+ 'default' => array('adapter' => 'Php')
+ ));
+
+ $key = 'write_key';
+ $value = 'write_value';
+
+ Session::write($key, $value);
+ $result = Session::read($key);
+ $this->assertEqual($value, $result);
+
+ $key2 = 'write_key_2';
+ $value2 = 'write_value_2';
+ Session::write($key2, $value2);
+ $result = Session::read($key2);
+ $this->assertEqual($value2, $result);
+
+ $this->assertTrue(Session::delete($key));
+ $this->assertTrue(Session::delete($key2));
+
+ $result = Session::read($key);
+ $result2 = Session::read($key2);
+
+ $this->assertNull($result);
+ $this->assertNull($result2);
+ }
+
+ public function testWriteReadDelete() {
+ Session::config(array(
+ 'default' => array('adapter' => 'Php')
+ ));
+
+ $key = 'test';
+ $value = 'value';
+
+ Session::write($key, $value);
+ $result = Session::read($key);
+
+ $this->assertEqual($value, $result);
+ $this->assertTrue(Session::delete($key));
+
+ $result = Session::read($key);
+ $this->assertNull($result);
+ }
+
+ public function testNamespaces() {
+ Session::config(array(
+ 'test' => array('adapter' => 'Php')
+ ));
+
+ $value = 'second value';
+ Session::write('first.second', $value);
+ $result = Session::read('first.second');
+ $this->assertEqual($value, $result);
+ $this->assertTrue(isset($_SESSION['first']));
+ $this->assertTrue(isset($_SESSION['first']['second']));
+ $this->assertEqual($value, $_SESSION['first']['second']);
+
+ $result = Session::read('first');
+ $expected = array('second' => 'second value');
+ $this->assertEqual($expected, $result);
+ $this->assertTrue(isset($_SESSION['first']));
+ $this->assertEqual($_SESSION['first'], $result);
+
+ $value = 'another value';
+ Session::write('first.sibling', $value);
+ $result = Session::read('first.sibling');
+ $this->assertEqual($value, $result);
+ $this->assertEqual($_SESSION['first']['sibling'], $value);
+
+ $result = Session::delete('first.sibling');
+ $this->assertEqual(true, $result);
+ $this->assertFalse(isset($_SESSION['first']['sibling']));
+ $this->assertTrue(isset($_SESSION['first']['second']));
+
+ $result = Session::delete('first');
+ $this->assertEqual(true, $result);
+ $this->assertFalse(isset($_SESSION['first']));
+ }
+
+ public function testCookieWriteReadDelete() {
+ Session::config(array(
+ 'li3' => array('adapter' => 'Cookie', 'expiry' => '+1 day')
+ ));
+
+ Session::write('testkey1', 'value1', array('name' => 'li3'));
+ Session::write('testkey2', 'value2', array('name' => 'li3'));
+ Session::write('testkey3', 'value3', array('name' => 'li3'));
+
+ $this->assertCookie(
+ array('key' => 'testkey1', 'value' => 'value1')
+ );
+ $this->assertCookie(
+ array('key' => 'testkey2', 'value' => 'value2')
+ );
+ $this->assertCookie(
+ array('key' => 'testkey3', 'value' => 'value3')
+ );
+
+ Session::delete('testkey1', array('name' => 'li3'));
+ Session::delete('testkey2', array('name' => 'li3'));
+ Session::delete('testkey3', array('name' => 'li3'));
+
+ $params = array('exires' => '-1 second', 'path' => '/');
+
+ $this->assertCookie(
+ array('key' => 'testkey1', 'value' => 'deleted')
+ );
+ $this->assertCookie(
+ array('key' => 'testkey2', 'value' => 'deleted')
+ );
+ $this->assertCookie(
+ array('key' => 'testkey3', 'value' => 'deleted')
+ );
+ }
+
+ public function testStrategiesPhpAdapter() {
+ Session::config(array(
+ 'strategy' => array(
+ 'adapter' => 'Php',
+ 'strategies' => array('Hmac' => array('secret' => 'somesecretkey'))
+ )
+ ));
+
+ $key = 'test';
+ $value = 'value';
+
+ Session::write($key, $value, array('name' => 'strategy'));
+ $result = Session::read($key, array('name' => 'strategy'));
+
+ $this->assertEqual($value, $result);
+ $this->assertTrue(Session::delete($key, array('name' => 'strategy')));
+ $result = Session::read($key, array('name' => 'strategy'));
+ $this->assertNull($result);
+
+ Session::write($key, $value, array('name' => 'strategy'));
+ $result = Session::read($key, array('name' => 'strategy'));
+ $this->assertEqual($value, $result);
+
+ $cache = $_SESSION;
+ $_SESSION['injectedkey'] = 'hax0r';
+ $this->expectException('/Possible data tampering - HMAC signature does not match data./');
+ $result = Session::read($key, array('name' => 'strategy'));
+ $_SESSION = $cache;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/integration/test/FilterTest.php b/libraries/lithium/tests/integration/test/FilterTest.php
new file mode 100644
index 0000000..7b9e5be
--- /dev/null
+++ b/libraries/lithium/tests/integration/test/FilterTest.php
@@ -0,0 +1,170 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\integration\test;
+
+use lithium\test\filter\Coverage;
+use lithium\test\Group;
+use lithium\test\Report;
+
+class FilterTest extends \lithium\test\Integration {
+
+ public function setUp() {
+ $this->report = new Report(array(
+ 'title' => '\lithium\tests\mocks\test\MockFilterTest',
+ 'group' => new Group(
+ array('data' => array('\lithium\tests\mocks\test\MockFilterClassTest'))
+ )
+ ));
+ }
+
+ public function testSingleTest() {
+ $this->report->filters(array("Coverage" => ""));
+
+ $this->report->run();
+
+ $expected = 40;
+
+ $filter = $this->report->results['filters']['lithium\test\filter\Coverage'];
+ $data = $filter['lithium\tests\mocks\test\MockFilterClass'];
+ $result = $data['percentage'];
+
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSingleTestWithMultipleFilters() {
+ $all = array(
+ "Coverage",
+ "Complexity",
+ "Profiler",
+ "Affected"
+ );
+
+ $permutations = $this->power_perms($all);
+
+ foreach ($permutations as $filters) {
+ $filters = array_flip($filters);
+ $filters = array_map(function($v) {
+ return "";
+ }, $filters);
+
+ $report = new Report(array(
+ 'title' => '\lithium\tests\mocks\test\MockFilterTest',
+ 'group' => new Group(
+ array('data' => array('\lithium\tests\mocks\test\MockFilterClassTest'))
+ )
+ ));
+
+ $report->filters($filters);
+
+ $report->run();
+
+ if (array_key_exists("Coverage", $filters)) {
+ $expected = 40;
+
+ $result = $report->results['filters'];
+
+ $this->assertTrue(isset($result['lithium\test\filter\Coverage']),
+ "Filter(s): '" . join(array_keys($filters), ", ") . "'"
+ . "returned no Coverage results."
+ );
+ $percentage = $result['lithium\test\filter\Coverage'];
+ $percentage = $percentage['lithium\tests\mocks\test\MockFilterClass'];
+ $percentage = $percentage['percentage'];
+
+ $this->assertEqual($expected, $percentage);
+ }
+ }
+ }
+
+ /**
+ * Methods for getting all permutations of each set in the power set of an array of strings
+ * (from the php.net manual on shuffle)
+ */
+
+ private function power_perms($arr) {
+ $power_set = $this->power_set($arr);
+ $result = array();
+ foreach($power_set as $set) {
+ $perms = $this->perms($set);
+ $result = array_merge($result,$perms);
+ }
+ return $result;
+ }
+
+ private function power_set($in,$minLength = 1) {
+ $count = count($in);
+ $members = pow(2,$count);
+ $return = array();
+ for ($i = 0; $i < $members; $i++) {
+ $b = sprintf("%0" . $count . "b", $i);
+ $out = array();
+ for ($j = 0; $j < $count; $j++) {
+ if ($b{$j} == '1') $out[] = $in[$j];
+ }
+ if (count($out) >= $minLength) {
+ $return[] = $out;
+ }
+ }
+
+ //usort($return,"cmp"); //can sort here by length
+ return $return;
+ }
+
+ private function factorial($int){
+ if($int < 2) {
+ return 1;
+ }
+
+ for($f = 2; $int - 1 > 1; $f *= $int--){}
+
+ return $f;
+ }
+
+ private function perm($arr, $nth = null) {
+
+ if ($nth === null) {
+ return $this->perms($arr);
+ }
+
+ $result = array();
+ $length = count($arr);
+
+ while ($length--) {
+ $f = $this->factorial($length);
+ $p = floor($nth / $f);
+ $result[] = $arr[$p];
+ $this->array_delete_by_key($arr, $p);
+ $nth -= $p * $f;
+ }
+
+ $result = array_merge($result,$arr);
+ return $result;
+ }
+
+ private function perms($arr) {
+ $p = array();
+ for ($i = 0; $i < $this->factorial(count($arr)); $i++) {
+ $p[] = $this->perm($arr, $i);
+ }
+ return $p;
+ }
+
+ private function array_delete_by_key(&$array, $delete_key, $use_old_keys = FALSE) {
+
+ unset($array[$delete_key]);
+
+ if(!$use_old_keys) {
+ $array = array_values($array);
+ }
+
+ return TRUE;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/action/MockCgiRequest.php b/libraries/lithium/tests/mocks/action/MockCgiRequest.php
index 86620eb..6ae2818 100644
--- a/libraries/lithium/tests/mocks/action/MockCgiRequest.php
+++ b/libraries/lithium/tests/mocks/action/MockCgiRequest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/action/MockControllerRequest.php b/libraries/lithium/tests/mocks/action/MockControllerRequest.php
index 2fa63bd..7efb3c8 100644
--- a/libraries/lithium/tests/mocks/action/MockControllerRequest.php
+++ b/libraries/lithium/tests/mocks/action/MockControllerRequest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/action/MockControllerResponse.php b/libraries/lithium/tests/mocks/action/MockControllerResponse.php
index 063486d..d899718 100644
--- a/libraries/lithium/tests/mocks/action/MockControllerResponse.php
+++ b/libraries/lithium/tests/mocks/action/MockControllerResponse.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/action/MockDispatcher.php b/libraries/lithium/tests/mocks/action/MockDispatcher.php
index d69e33a..8387a0c 100644
--- a/libraries/lithium/tests/mocks/action/MockDispatcher.php
+++ b/libraries/lithium/tests/mocks/action/MockDispatcher.php
@@ -8,7 +8,7 @@
namespace lithium\tests\mocks\action;
-use \stdClass;
+use stdClass;
class MockDispatcher extends \lithium\action\Dispatcher {
@@ -21,6 +21,9 @@ class MockDispatcher extends \lithium\action\Dispatcher {
}
protected static function _call($callable, $request, $params) {
+ if (is_callable($callable->params['controller'])) {
+ return parent::_call($callable->params['controller'], $request, $params);
+ }
static::$dispatched[] = $callable;
}
}
diff --git a/libraries/lithium/tests/mocks/action/MockIisRequest.php b/libraries/lithium/tests/mocks/action/MockIisRequest.php
index c6731b8..7c483b7 100644
--- a/libraries/lithium/tests/mocks/action/MockIisRequest.php
+++ b/libraries/lithium/tests/mocks/action/MockIisRequest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/action/MockMediaClass.php b/libraries/lithium/tests/mocks/action/MockMediaClass.php
index 5b51151..b4db02f 100644
--- a/libraries/lithium/tests/mocks/action/MockMediaClass.php
+++ b/libraries/lithium/tests/mocks/action/MockMediaClass.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/action/MockPostsController.php b/libraries/lithium/tests/mocks/action/MockPostsController.php
index 592f78e..03a1d3e 100644
--- a/libraries/lithium/tests/mocks/action/MockPostsController.php
+++ b/libraries/lithium/tests/mocks/action/MockPostsController.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -27,10 +27,10 @@ class MockPostsController extends \lithium\action\Controller {
}
public function send() {
- $this->redirect('/posts');
+ $this->redirect('/posts', array('exit' => true));
}
- public function type() {
+ public function type($raw = false) {
return array('data' => 'test');
}
@@ -47,7 +47,7 @@ class MockPostsController extends \lithium\action\Controller {
}
public function view2($id = null) {
- $this->render('view');
+ $this->render(array('template' => 'view'));
}
public function view3($id = null) {
@@ -66,7 +66,7 @@ class MockPostsController extends \lithium\action\Controller {
return $this->{$var};
}
- protected function _stop() {
+ protected function _stop($status = 0 ) {
$this->stopped = true;
}
}
diff --git a/libraries/lithium/tests/mocks/action/MockRequestType.php b/libraries/lithium/tests/mocks/action/MockRequestType.php
index 9e13c49..6731f05 100644
--- a/libraries/lithium/tests/mocks/action/MockRequestType.php
+++ b/libraries/lithium/tests/mocks/action/MockRequestType.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -10,7 +10,11 @@ namespace lithium\tests\mocks\action;
class MockRequestType extends \lithium\action\Request {
- public function type() {
+ public function type($raw = false) {
+ return 'foo';
+ }
+
+ public function accepts($type = null) {
return 'foo';
}
}
diff --git a/libraries/lithium/tests/mocks/action/MockResponse.php b/libraries/lithium/tests/mocks/action/MockResponse.php
index 6412bd8..f459245 100644
--- a/libraries/lithium/tests/mocks/action/MockResponse.php
+++ b/libraries/lithium/tests/mocks/action/MockResponse.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/analysis/MockLoggerAdapter.php b/libraries/lithium/tests/mocks/analysis/MockLoggerAdapter.php
index 3f8055c..aff1675 100644
--- a/libraries/lithium/tests/mocks/analysis/MockLoggerAdapter.php
+++ b/libraries/lithium/tests/mocks/analysis/MockLoggerAdapter.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/console/MockCommand.php b/libraries/lithium/tests/mocks/console/MockCommand.php
index 93f7ae3..32a6deb 100644
--- a/libraries/lithium/tests/mocks/console/MockCommand.php
+++ b/libraries/lithium/tests/mocks/console/MockCommand.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/console/MockDispatcherCommand.php b/libraries/lithium/tests/mocks/console/MockDispatcherCommand.php
index f1b83e3..8be9d32 100644
--- a/libraries/lithium/tests/mocks/console/MockDispatcherCommand.php
+++ b/libraries/lithium/tests/mocks/console/MockDispatcherCommand.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/console/MockDispatcherRequest.php b/libraries/lithium/tests/mocks/console/MockDispatcherRequest.php
index 128ff9d..2b48283 100644
--- a/libraries/lithium/tests/mocks/console/MockDispatcherRequest.php
+++ b/libraries/lithium/tests/mocks/console/MockDispatcherRequest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/console/MockResponse.php b/libraries/lithium/tests/mocks/console/MockResponse.php
index 9fcca6c..39e99f8 100644
--- a/libraries/lithium/tests/mocks/console/MockResponse.php
+++ b/libraries/lithium/tests/mocks/console/MockResponse.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\console;
-use \lithium\util\String;
+use lithium\util\String;
class MockResponse extends \lithium\console\Response {
diff --git a/libraries/lithium/tests/mocks/console/command/MockCommandHelp.php b/libraries/lithium/tests/mocks/console/command/MockCommandHelp.php
index 53fc6ac..70dbe29 100644
--- a/libraries/lithium/tests/mocks/console/command/MockCommandHelp.php
+++ b/libraries/lithium/tests/mocks/console/command/MockCommandHelp.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -27,7 +27,7 @@ class MockCommandHelp extends \lithium\console\Command {
* @var boolean
*/
public $s = true;
-
+
/**
* Don't show this.
*
@@ -64,17 +64,17 @@ class MockCommandHelp extends \lithium\console\Command {
* @param string $arg2
* @return boolean
*/
- public function sampleTaskWithOptionalArgs($arg1 = null, $arg2 = nill) {
+ public function sampleTaskWithOptionalArgs($arg1 = null, $arg2 = null) {
return true;
}
-
+
/**
* Don't show in the help
*
* @return void
*/
protected function _sampleHelper() {
-
+
}
}
diff --git a/libraries/lithium/tests/mocks/console/command/MockCreate.php b/libraries/lithium/tests/mocks/console/command/MockCreate.php
index 743d15d..7739f20 100644
--- a/libraries/lithium/tests/mocks/console/command/MockCreate.php
+++ b/libraries/lithium/tests/mocks/console/command/MockCreate.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/console/command/MockLibraryService.php b/libraries/lithium/tests/mocks/console/command/MockLibraryService.php
index d9f2f30..7467dac 100644
--- a/libraries/lithium/tests/mocks/console/command/MockLibraryService.php
+++ b/libraries/lithium/tests/mocks/console/command/MockLibraryService.php
@@ -2,24 +2,28 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\console\command;
+use lithium\core\Libraries;
use lithium\net\http\Response;
class MockLibraryService extends \lithium\net\http\Service {
public function send($method, $path = null, $data = array(), array $options = array()) {
+ if ($this->_config['host'] == 'localhost') {
+ return null;
+ }
if ($method == 'post') {
$this->request = $this->_request($method, $path, $data, $options);
- if (!empty($this->request->auth['username'])) {
+ if (!empty($this->request->username)) {
$user = array(
'method' => 'Basic', 'username' => 'gwoo', 'password' => 'password'
);
- if ($this->request->auth !== $user) {
+ if ($this->request->username !== $user['username']) {
$this->last = (object) array('response' => new Response());
$this->last->response->status(401);
return json_encode(array(
@@ -43,12 +47,20 @@ class MockLibraryService extends \lithium\net\http\Service {
if (preg_match("/lab\/extensions/", $path, $match)) {
return json_encode($this->__data('extensions'));
}
- if (preg_match("/lab\/(.*?).json/", $path, $match)) {
+ if (preg_match("/lab\/li3_lab.json/", $path, $match)) {
+ return json_encode($this->__data('plugins', 0));
+ }
+ if (preg_match("/lab\/library_test_plugin.json/", $path, $match)) {
return json_encode($this->__data('plugins', 1));
}
+ if (preg_match("/lab\/li3_docs.json/", $path, $match)) {
+ return json_encode($this->__data('plugins', 2));
+ }
}
private function __data($type, $key = null) {
+ $resources = Libraries::get(true, 'resources');
+
$plugins = array(
array(
'name' => 'li3_lab', 'version' => '1.0',
@@ -80,12 +92,29 @@ class MockLibraryService extends \lithium\net\http\Service {
'created' => '2009-11-30', 'updated' => '2009-11-30',
'rating' => '9.9', 'downloads' => '1000',
'sources' => array(
- 'phar' => LITHIUM_APP_PATH . '/resources/tmp/tests/library_test_plugin.phar.gz'
+ 'phar' => "{$resources}/tmp/tests/library_test_plugin.phar.gz"
),
'requires' => array(
'li3_lab' => array('version' => '<=1.0')
)
),
+ array(
+ 'name' => 'li3_docs', 'version' => '1.0',
+ 'summary' => 'the li3 plugin client/server',
+ 'maintainers' => array(
+ array(
+ 'name' => 'gwoo', 'email' => 'gwoo@nowhere.com',
+ 'website' => 'li3.rad-dev.org'
+ )
+ ),
+ 'created' => '2009-11-30', 'updated' => '2009-11-30',
+ 'rating' => '9.9', 'downloads' => '1000',
+ 'sources' => array(
+ 'git' => 'git://rad-dev.org/li3_docs.git',
+ 'phar' => 'http://downloads.rad-dev.org/li3_docs.phar.gz'
+ ),
+ 'requires' => array()
+ ),
);
$extensions = array(
diff --git a/libraries/lithium/tests/mocks/core/MockAdapter.php b/libraries/lithium/tests/mocks/core/MockAdapter.php
index a7fa5c1..7bb223f 100644
--- a/libraries/lithium/tests/mocks/core/MockAdapter.php
+++ b/libraries/lithium/tests/mocks/core/MockAdapter.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -10,7 +10,7 @@ namespace lithium\tests\mocks\core;
class MockAdapter extends \lithium\core\Adaptable {
- protected static $_configurations = null;
+ protected static $_configurations = array();
protected static $_adapters = 'adapter.storage.cache';
}
diff --git a/libraries/lithium/tests/mocks/core/MockCallable.php b/libraries/lithium/tests/mocks/core/MockCallable.php
index 5549fa0..f9084c6 100644
--- a/libraries/lithium/tests/mocks/core/MockCallable.php
+++ b/libraries/lithium/tests/mocks/core/MockCallable.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/core/MockExposed.php b/libraries/lithium/tests/mocks/core/MockExposed.php
index a02f1f0..b4cc760 100644
--- a/libraries/lithium/tests/mocks/core/MockExposed.php
+++ b/libraries/lithium/tests/mocks/core/MockExposed.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/core/MockInstantiator.php b/libraries/lithium/tests/mocks/core/MockInstantiator.php
new file mode 100644
index 0000000..e396004
--- /dev/null
+++ b/libraries/lithium/tests/mocks/core/MockInstantiator.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\core;
+
+class MockInstantiator extends \lithium\core\Object {
+
+ protected $_classes = array('request' => '\lithium\tests\mocks\core\MockRequest');
+
+ public function instance($name, array $config = array()) {
+ return $this->_instance($name, $config);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/core/MockMethodFiltering.php b/libraries/lithium/tests/mocks/core/MockMethodFiltering.php
index 9902979..65fbac4 100644
--- a/libraries/lithium/tests/mocks/core/MockMethodFiltering.php
+++ b/libraries/lithium/tests/mocks/core/MockMethodFiltering.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/core/MockObjectConfiguration.php b/libraries/lithium/tests/mocks/core/MockObjectConfiguration.php
index 3b9d271..094e7e5 100644
--- a/libraries/lithium/tests/mocks/core/MockObjectConfiguration.php
+++ b/libraries/lithium/tests/mocks/core/MockObjectConfiguration.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/core/MockObjectForParents.php b/libraries/lithium/tests/mocks/core/MockObjectForParents.php
index 45dd63b..d32547e 100644
--- a/libraries/lithium/tests/mocks/core/MockObjectForParents.php
+++ b/libraries/lithium/tests/mocks/core/MockObjectForParents.php
@@ -2,14 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\core;
class MockObjectForParents extends \lithium\core\Object {
- static public function parents() {
+
+ public static function parents() {
return static::_parents();
}
}
diff --git a/libraries/lithium/tests/mocks/core/MockRequest.php b/libraries/lithium/tests/mocks/core/MockRequest.php
index 389fcb7..a8870f3 100644
--- a/libraries/lithium/tests/mocks/core/MockRequest.php
+++ b/libraries/lithium/tests/mocks/core/MockRequest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/core/MockStaticFilteringExtended.php b/libraries/lithium/tests/mocks/core/MockStaticFilteringExtended.php
index bf38844..b4489b5 100644
--- a/libraries/lithium/tests/mocks/core/MockStaticFilteringExtended.php
+++ b/libraries/lithium/tests/mocks/core/MockStaticFilteringExtended.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/core/MockStaticInstantiator.php b/libraries/lithium/tests/mocks/core/MockStaticInstantiator.php
new file mode 100644
index 0000000..823830a
--- /dev/null
+++ b/libraries/lithium/tests/mocks/core/MockStaticInstantiator.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\core;
+
+class MockStaticInstantiator extends \lithium\core\StaticObject {
+
+ protected static $_classes = array('request' => '\lithium\tests\mocks\core\MockRequest');
+
+ public static function instance($name, array $config = array()) {
+ return static::_instance($name, $config);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/core/MockStaticMethodFiltering.php b/libraries/lithium/tests/mocks/core/MockStaticMethodFiltering.php
index b5ea1d1..e81f30e 100644
--- a/libraries/lithium/tests/mocks/core/MockStaticMethodFiltering.php
+++ b/libraries/lithium/tests/mocks/core/MockStaticMethodFiltering.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -45,6 +45,16 @@ class MockStaticMethodFiltering extends \lithium\core\StaticObject {
$args = func_get_args();
return $args;
}
+
+ public static function parents($get = false) {
+ if ($get === null) {
+ static::$_parents = array();
+ }
+ if ($get) {
+ return static::$_parents;
+ }
+ return static::_parents();
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/core/MockStrategy.php b/libraries/lithium/tests/mocks/core/MockStrategy.php
index c0895e1..6091562 100644
--- a/libraries/lithium/tests/mocks/core/MockStrategy.php
+++ b/libraries/lithium/tests/mocks/core/MockStrategy.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -10,7 +10,7 @@ namespace lithium\tests\mocks\core;
class MockStrategy extends \lithium\core\Adaptable {
- protected static $_configurations = null;
+ protected static $_configurations = array();
protected static $_strategies = 'strategy.storage.cache';
}
diff --git a/libraries/lithium/tests/mocks/data/Companies.php b/libraries/lithium/tests/mocks/data/Companies.php
new file mode 100644
index 0000000..5c3a5df
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/Companies.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data;
+
+class Companies extends \lithium\data\Model {
+
+ public $hasMany = array('Employees');
+
+ protected $_meta = array('connection' => 'test');
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/Employees.php b/libraries/lithium/tests/mocks/data/Employees.php
new file mode 100644
index 0000000..fa7e549
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/Employees.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data;
+
+class Employees extends \lithium\data\Model {
+
+ public $belongsTo = array('Companies');
+
+ protected $_meta = array('connection' => 'test');
+
+ public function lastName($entity) {
+ $name = explode(' ', $entity->name);
+ return $name[1];
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/MockBase.php b/libraries/lithium/tests/mocks/data/MockBase.php
new file mode 100644
index 0000000..b7ae501
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/MockBase.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD(http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data;
+
+class MockBase extends \lithium\data\Model {
+
+ protected $_meta = array('connection' => 'mock-source');
+
+ public static function __init() {
+ static::_isBase(__CLASS__, true);
+ parent::__init();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/MockComment.php b/libraries/lithium/tests/mocks/data/MockComment.php
index 45072ff..b05fedd 100644
--- a/libraries/lithium/tests/mocks/data/MockComment.php
+++ b/libraries/lithium/tests/mocks/data/MockComment.php
@@ -2,23 +2,21 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data;
-use \lithium\data\model\Query;
-use \lithium\data\model\Record;
-use \lithium\data\collection\RecordSet;
+use lithium\data\model\Query;
+use lithium\data\entity\Record;
+use lithium\data\collection\RecordSet;
-class MockComment extends \lithium\data\Model {
+class MockComment extends \lithium\tests\mocks\data\MockBase {
public $belongsTo = array('MockPost');
- protected $_meta = array(
- 'connection' => 'mock-source', 'key' => 'comment_id'
- );
+ protected $_meta = array('key' => 'comment_id');
public static function find($type, array $options = array()) {
$defaults = array(
@@ -26,7 +24,7 @@ class MockComment extends \lithium\data\Model {
);
$options += $defaults;
$params = compact('type', 'options');
- $self = static::_instance();
+ $self = static::_object();
$filter = function($self, $params) {
extract($params);
@@ -34,7 +32,7 @@ class MockComment extends \lithium\data\Model {
return new RecordSet(array(
'query' => $query,
- 'items' => array_map(
+ 'data' => array_map(
function($data) { return new Record(compact('data')); },
array(
array('comment_id' => 1, 'author_id' => 123, 'text' => 'First comment'),
diff --git a/libraries/lithium/tests/mocks/data/MockCouchModel.php b/libraries/lithium/tests/mocks/data/MockCouchModel.php
new file mode 100644
index 0000000..c87ef79
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/MockCouchModel.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data;
+
+class MockCouchModel extends \lithium\data\Model {
+
+ protected $_schema = array(
+ 'someKey' => array()
+ );
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/MockCreator.php b/libraries/lithium/tests/mocks/data/MockCreator.php
index 57af9e9..e10cc00 100644
--- a/libraries/lithium/tests/mocks/data/MockCreator.php
+++ b/libraries/lithium/tests/mocks/data/MockCreator.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/data/MockModel.php b/libraries/lithium/tests/mocks/data/MockModel.php
index 56e9333..25d67e4 100644
--- a/libraries/lithium/tests/mocks/data/MockModel.php
+++ b/libraries/lithium/tests/mocks/data/MockModel.php
@@ -2,16 +2,18 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data;
+use lithium\tests\mocks\data\source\database\adapter\MockAdapter;
+
class MockModel extends \lithium\data\Model {
public static function key($values = array('id' => null)) {
- $key = static::_instance()->_meta['key'];
+ $key = static::_object()->_meta['key'];
if (method_exists($values, 'to')) {
$values = $values->to('array');
@@ -21,7 +23,15 @@ class MockModel extends \lithium\data\Model {
return $values[$key];
}
- public static function __init(array $options = array()) {}
+ public static function __init() {}
+
+ public static function &connection($records = null) {
+ $mock = new MockAdapter(compact('records') + array(
+ 'columns' => array('lithium\tests\mocks\data\MockModel' => array('id', 'data')),
+ 'autoConnect' => false
+ ));
+ return $mock;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/MockPost.php b/libraries/lithium/tests/mocks/data/MockPost.php
index 7b6f4b8..38240a5 100644
--- a/libraries/lithium/tests/mocks/data/MockPost.php
+++ b/libraries/lithium/tests/mocks/data/MockPost.php
@@ -2,25 +2,36 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD(http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD(http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data;
-class MockPost extends \lithium\data\Model {
+class MockPost extends \lithium\tests\mocks\data\MockBase {
public $hasMany = array('MockComment');
- protected $_meta = array('connection' => 'mock-source');
+ public static $connection = null;
public static function resetSchema() {
- static::_instance()->_schema = array();
+ static::_object()->_schema = array();
+ }
+
+ public static function overrideSchema(array $schema = array()) {
+ static::_object()->_schema = $schema;
}
public static function instances() {
return array_keys(static::$_instances);
}
+
+ public static function &connection() {
+ if (static::$connection) {
+ return static::$connection;
+ }
+ return parent::connection();
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/MockPostForValidates.php b/libraries/lithium/tests/mocks/data/MockPostForValidates.php
index d97c6ec..da33778 100644
--- a/libraries/lithium/tests/mocks/data/MockPostForValidates.php
+++ b/libraries/lithium/tests/mocks/data/MockPostForValidates.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/data/MockPostObject.php b/libraries/lithium/tests/mocks/data/MockPostObject.php
index 4dfaa0b..767c904 100644
--- a/libraries/lithium/tests/mocks/data/MockPostObject.php
+++ b/libraries/lithium/tests/mocks/data/MockPostObject.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD(http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD(http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -21,4 +21,4 @@ class MockPostObject {
}
}
-?>
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/MockSource.php b/libraries/lithium/tests/mocks/data/MockSource.php
index 8ef8c4d..604c2b2 100644
--- a/libraries/lithium/tests/mocks/data/MockSource.php
+++ b/libraries/lithium/tests/mocks/data/MockSource.php
@@ -2,69 +2,23 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data;
+use lithium\util\Inflector;
+
class MockSource extends \lithium\data\Source {
protected $_classes = array(
- 'record' => '\lithium\data\model\Record',
- 'recordSet' => '\lithium\data\collection\RecordSet'
+ 'entity' => 'lithium\data\entity\Record',
+ 'set' => 'lithium\data\collection\RecordSet',
+ 'relationship' => 'lithium\data\model\Relationship'
);
- public function connect() {
- return true;
- }
-
- public function disconnect() {
- return true;
- }
-
- public function entities($class = null) {
- return array('mock_posts', 'mock_comments', 'mock_tags', 'posts_tags');
- }
-
- public function item($model, array $data = array(), array $options = array()) {
- $class = $this->_classes['record'];
- return new $class(compact('model', 'data') + $options);
- }
-
- public function describe($entity, $meta = array()) {
- $var = "__{$entity}";
- if ($this->{$var}) {
- return $this->{$var};
- }
- return array();
- }
-
- public function create($query, array $options = array()) {
- return array($query => $options);
- }
-
- public function read($query, array $options = array()) {
- return compact('query', 'options');
- }
-
- public function update($query, array $options = array()) {
- return compact('query', 'options');
- }
-
- public function delete($query, array $options = array()) {
- return compact('query', 'options');
- }
-
- public function schema($query, $resource = null, $context = null) {
-
- }
-
- public function result($type, $resource, $context) {
-
- }
-
- private $__mock_posts = array(
+ private $_mockPosts = array(
'id' => array('type' => 'int', 'length' => '10', 'null' => false, 'default' => NULL),
'user_id' => array(
'type' => 'int', 'length' => '10', 'null' => true, 'default' => NULL
@@ -86,7 +40,7 @@ class MockSource extends \lithium\data\Source {
)
);
- private $__mock_comments = array(
+ private $_mockComments = array(
'id' => array(
'type' => 'int', 'length' => '10', 'null' => false, 'default' => NULL,
),
@@ -116,7 +70,7 @@ class MockSource extends \lithium\data\Source {
),
);
- private $__mock_tags = array(
+ private $_mockTags = array(
'id' => array(
'type' => 'int', 'length' => '10', 'null' => false, 'default' => NULL,
),
@@ -130,6 +84,71 @@ class MockSource extends \lithium\data\Source {
'type' => 'varchar', 'length' => '20', 'null' => true, 'default' => NULL,
),
);
+
+ public function connect() {
+ return ($this->_isConnected = true);
+ }
+
+ public function disconnect() {
+ return !($this->_isConnected = false);
+ }
+
+ public function entities($class = null) {
+ return array('mock_posts', 'mock_comments', 'mock_tags', 'posts_tags');
+ }
+
+ public function describe($entity, array $meta = array()) {
+ $var = "_" . Inflector::camelize($entity, false);
+ if ($this->{$var}) {
+ return $this->{$var};
+ }
+ return array();
+ }
+
+ public function create($query, array $options = array()) {
+ return compact('query', 'options');
+ }
+
+ public function read($query, array $options = array()) {
+ return compact('query', 'options');
+ }
+
+ public function update($query, array $options = array()) {
+ return compact('query', 'options');
+ }
+
+ public function delete($query, array $options = array()) {
+ return compact('query', 'options');
+ }
+
+ public function schema($query, $resource = null, $context = null) {
+
+ }
+
+ public function result($type, $resource, $context) {
+
+ }
+
+ public function cast($entity, array $data = array(), array $options = array()) {
+ $defaults = array('first' => false);
+ $options += $defaults;
+ return $options['first'] ? reset($data) : $data;
+ }
+
+ public function relationship($class, $type, $name, array $options = array()) {
+ $keys = Inflector::underscore($type == 'belongsTo' ? $name : $class::meta('name')) . '_id';
+
+ $options += compact('name', 'type', 'keys');
+ $options['from'] = $class;
+
+ $relationship = $this->_classes['relationship'];
+ return new $relationship($options);
+ }
+
+ public function calculation($type, $query, array $options = array()) {
+ $query->calculate($type);
+ return compact('query', 'options');
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/MockTag.php b/libraries/lithium/tests/mocks/data/MockTag.php
index 2003a10..6abfaf8 100644
--- a/libraries/lithium/tests/mocks/data/MockTag.php
+++ b/libraries/lithium/tests/mocks/data/MockTag.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/data/MockTagging.php b/libraries/lithium/tests/mocks/data/MockTagging.php
index 99d48b6..2ce4bae 100644
--- a/libraries/lithium/tests/mocks/data/MockTagging.php
+++ b/libraries/lithium/tests/mocks/data/MockTagging.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/data/collection/MockRecordSet.php b/libraries/lithium/tests/mocks/data/collection/MockRecordSet.php
index d93ac87..1aa267f 100644
--- a/libraries/lithium/tests/mocks/data/collection/MockRecordSet.php
+++ b/libraries/lithium/tests/mocks/data/collection/MockRecordSet.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/data/model/MockDatabase.php b/libraries/lithium/tests/mocks/data/model/MockDatabase.php
index b9c0e15..8738661 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDatabase.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDatabase.php
@@ -2,16 +2,20 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data\model;
+use lithium\tests\mocks\data\model\mock_database\MockResult;
+
class MockDatabase extends \lithium\data\source\Database {
public $sql = null;
+ protected $_quotes = array('{', '}');
+
public function connect() {
return true;
}
@@ -22,7 +26,7 @@ class MockDatabase extends \lithium\data\source\Database {
public function entities($class = null) {}
- public function describe($entity, $meta = array()) {}
+ public function describe($entity, array $meta = array()) {}
public function encoding($encoding = null) {}
@@ -30,17 +34,33 @@ class MockDatabase extends \lithium\data\source\Database {
public function error() {}
- protected function _execute($sql) {
- return $this->sql = $sql;
+ public function value($value, array $schema = array()) {
+ if (($result = parent::value($value, $schema)) !== null) {
+ return $result;
+ }
+ return "'{$value}'";
}
- protected function _insertId($query) {
- return sha1(serialize($query));
+ public function cast($entity, array $data, array $options = array()) {
+ $defaults = array('first' => false);
+ $options += $defaults;
+ return $options['first'] ? reset($data) : $data;
}
public function testConfig() {
return $this->_config;
}
+
+ protected function _execute($sql) {
+ $this->sql = $sql;
+ return new MockResult();
+ }
+
+ protected function _insertId($query) {
+ $query = $query->export($this);
+ ksort($query);
+ return sha1(serialize($query));
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/model/MockDatabaseComment.php b/libraries/lithium/tests/mocks/data/model/MockDatabaseComment.php
index 877f558..94806df 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDatabaseComment.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDatabaseComment.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -12,6 +12,10 @@ class MockDatabaseComment extends \lithium\data\Model {
public $belongsTo = array('MockDatabasePost');
+ protected $_meta = array(
+ 'connection' => 'mock-database-connection'
+ );
+
protected $_schema = array(
'id' => array('type' => 'integer'),
'post_id' => array('type' => 'integer'),
diff --git a/libraries/lithium/tests/mocks/data/model/MockDatabasePost.php b/libraries/lithium/tests/mocks/data/model/MockDatabasePost.php
index 8bb747b..b121fee 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDatabasePost.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDatabasePost.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -12,8 +12,13 @@ class MockDatabasePost extends \lithium\data\Model {
public $hasMany = array('MockDatabaseComment');
+ protected $_meta = array(
+ 'connection' => 'mock-database-connection'
+ );
+
protected $_schema = array(
'id' => array('type' => 'integer'),
+ 'author_id' => array('type' => 'integer'),
'title' => array('type' => 'string'),
'created' => array('type' => 'datetime')
);
diff --git a/libraries/lithium/tests/mocks/data/model/MockDatabaseTag.php b/libraries/lithium/tests/mocks/data/model/MockDatabaseTag.php
index 82fe882..8f7a7bc 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDatabaseTag.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDatabaseTag.php
@@ -12,6 +12,10 @@ class MockDatabaseTag extends \lithium\data\Model {
public $hasMany = array('MockDatabaseTagging');
+ protected $_meta = array(
+ 'connection' => 'mock-database-connection'
+ );
+
protected $_schema = array(
'id' => array('type' => 'integer'),
'title' => array('type' => 'string'),
diff --git a/libraries/lithium/tests/mocks/data/model/MockDatabaseTagging.php b/libraries/lithium/tests/mocks/data/model/MockDatabaseTagging.php
index 2dedfe3..9fa29e8 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDatabaseTagging.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDatabaseTagging.php
@@ -12,6 +12,10 @@ class MockDatabaseTagging extends \lithium\data\Model {
public $belongsTo = array('MockDatabasePost', 'MockDatabaseTag');
+ protected $_meta = array(
+ 'connection' => 'mock-database-connection'
+ );
+
protected $_schema = array(
'id' => array('type' => 'integer'),
'post_id' => array('type' => 'integer'),
diff --git a/libraries/lithium/tests/mocks/data/model/MockDocumentMultipleKey.php b/libraries/lithium/tests/mocks/data/model/MockDocumentMultipleKey.php
index d6ecade..a33bb9a 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDocumentMultipleKey.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDocumentMultipleKey.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data\model;
-use \lithium\data\collection\Document;
+use lithium\data\entity\Document;
class MockDocumentMultipleKey extends \lithium\data\Model {
@@ -41,16 +41,23 @@ class MockDocumentMultipleKey extends \lithium\data\Model {
public static function find($type = 'all', array $options = array()) {
switch ($type) {
case 'first':
- return new Document(array('items' =>
- array('id' => 2, 'rev' => '1-1', 'name' => 'Two', 'content' => 'Lorem ipsum two')
- ));
+ return new Document(array('data' => array(
+ 'id' => 2, 'rev' => '1-1', 'name' => 'Two', 'content' => 'Lorem ipsum two'
+ )));
break;
case 'all':
default :
- return new Document(array('items' => array(
- array('id' => 1, 'rev' => '1-1','name' => 'One', 'content' => 'Lorem ipsum one'),
- array('id' => 2, 'rev' => '1-1','name' => 'Two', 'content' => 'Lorem ipsum two'),
- array('id' => 3, 'rev' => '1-1', 'name' => 'Three', 'content' => 'Lorem ipsum three')
+ return new Document(array('data' => array(
+ array(
+ 'id' => 1, 'rev' => '1-1','name' => 'One', 'content' => 'Lorem ipsum one'
+ ),
+ array(
+ 'id' => 2, 'rev' => '1-1','name' => 'Two', 'content' => 'Lorem ipsum two'
+ ),
+ array(
+ 'id' => 3, 'rev' => '1-1', 'name' => 'Three',
+ 'content' => 'Lorem ipsum three'
+ )
)));
break;
}
diff --git a/libraries/lithium/tests/mocks/data/model/MockDocumentPost.php b/libraries/lithium/tests/mocks/data/model/MockDocumentPost.php
index 68cd155..e2dbc89 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDocumentPost.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDocumentPost.php
@@ -2,20 +2,28 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data\model;
-use \lithium\data\collection\Document;
+use lithium\data\entity\Document;
+use lithium\data\collection\DocumentSet;
class MockDocumentPost extends \lithium\data\Model {
- public static function __init(array $options = array()) {}
+ protected $_meta = array('connection' => 'mongo');
+
+ protected static $_connection;
+
+ public static function __init() {}
public static function schema($field = null) {
- return array();
+ return array(
+ '_id' => array('type' => 'id'),
+ 'foo.bar' => array('type' => 'int')
+ );
}
public function ret($record, $param1 = null, $param2 = null) {
@@ -32,20 +40,31 @@ class MockDocumentPost extends \lithium\data\Model {
return 'lithium';
}
+ public static function &connection() {
+ if (!static::$_connection) {
+ static::$_connection = new MockDocumentSource();
+ }
+ return static::$_connection;
+ }
+
public static function find($type = 'all', array $options = array()) {
switch ($type) {
case 'first':
- return new Document(array('items' =>
- array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two')
+ return new Document(array(
+ 'data' => array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two'),
+ 'model' => __CLASS__
));
break;
case 'all':
default :
- return new Document(array('items' => array(
- array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one'),
- array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two'),
- array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three')
- )));
+ return new DocumentSet(array(
+ 'data' => array(
+ array('id' => 1, 'name' => 'One', 'content' => 'Lorem ipsum one'),
+ array('id' => 2, 'name' => 'Two', 'content' => 'Lorem ipsum two'),
+ array('id' => 3, 'name' => 'Three', 'content' => 'Lorem ipsum three')
+ ),
+ 'model' => __CLASS__
+ ));
break;
}
}
diff --git a/libraries/lithium/tests/mocks/data/model/MockDocumentSource.php b/libraries/lithium/tests/mocks/data/model/MockDocumentSource.php
index f548d6f..40ef566 100644
--- a/libraries/lithium/tests/mocks/data/model/MockDocumentSource.php
+++ b/libraries/lithium/tests/mocks/data/model/MockDocumentSource.php
@@ -2,21 +2,32 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\data\model;
+use MongoId;
+use MongoDate;
+use lithium\data\model\Relationship;
+
class MockDocumentSource extends \lithium\data\Source {
- public function connect() { }
+ protected $_classes = array(
+ 'entity' => 'lithium\data\entity\Document',
+ 'array' => 'lithium\data\collection\DocumentArray',
+ 'set' => 'lithium\data\collection\DocumentSet',
+ 'relationship' => 'lithium\data\model\Relationship'
+ );
+
+ public function connect() {}
public function disconnect() {}
public function entities($class = null) {}
- public function describe($entity, $meta = array()) {}
+ public function describe($entity, array $meta = array()) {}
public function create($query, array $options = array()) {}
public function update($query, array $options = array()) {}
- public function delete($query, array $options = array() ) {}
+ public function delete($query, array $options = array()) {}
protected $point = 0;
protected $result = null;
@@ -29,13 +40,90 @@ class MockDocumentSource extends \lithium\data\Source {
array('id' => 3, 'name' => 'Roe')
);
}
- public function hasNext() {
- return (is_array($this->result) && sizeof($this->result) > $this->point);
- }
+
public function getNext() {
return $this->result[$this->point++];
}
+ public function cast($entity, array $data, array $options = array()) {
+ $defaults = array('schema' => null, 'first' => false, 'pathKey' => null, 'arrays' => true);
+ $options += $defaults;
+ $model = null;
+
+ if (!$data) {
+ return $data;
+ }
+
+ if ($entity && !$options['schema']) {
+ $options['schema'] = $entity->schema() ?: array('_id' => array('type' => 'id'));
+ }
+ if ($entity) {
+ $model = $entity->model();
+ }
+ $schema = $options['schema'];
+ unset($options['schema']);
+
+ $handlers = array(
+ 'id' => function($v) {
+ return is_string($v) && preg_match('/^[0-9a-f]{24}$/', $v) ? new MongoId($v) : $v;
+ },
+ 'date' => function($v) {
+ $v = is_numeric($v) ? intval($v) : strtotime($v);
+ return (time() == $v) ? new MongoDate() : new MongoDate($v);
+ },
+ 'regex' => function($v) { return new MongoRegex($v); },
+ 'integer' => function($v) { return (integer) $v; },
+ 'float' => function($v) { return (float) $v; },
+ 'boolean' => function($v) { return (boolean) $v; },
+ 'code' => function($v) { return new MongoCode($v); },
+ 'binary' => function($v) { return new MongoBinData($v); },
+ );
+
+ $typeMap = array(
+ 'MongoId' => 'id',
+ 'MongoDate' => 'date',
+ 'MongoCode' => 'code',
+ 'MongoBinData' => 'binary',
+ 'datetime' => 'date',
+ 'timestamp' => 'date',
+ 'int' => 'integer'
+ );
+
+ foreach ($data as $key => $value) {
+ if (is_object($value)) {
+ continue;
+ }
+ $path = is_int($key) ? null : $key;
+ $path = $options['pathKey'] ? trim("{$options['pathKey']}.{$path}", '.') : $path;
+ $field = (isset($schema[$path]) ? $schema[$path] : array());
+ $field += array('type' => null, 'array' => null);
+ $type = isset($typeMap[$field['type']]) ? $typeMap[$field['type']] : $field['type'];
+ $isObject = ($type == 'object');
+ $isArray = (is_array($value) && $field['array'] !== false && !$isObject);
+
+ if (isset($handlers[$type])) {
+ $handler = $handlers[$type];
+ $value = $isArray ? array_map($handler, $value) : $handler($value);
+ }
+ if (!$options['arrays']) {
+ $data[$key] = $value;
+ continue;
+ }
+ $pathKey = $path;
+
+ if (is_array($value)) {
+ $arrayType = !$isObject && (array_keys($value) === range(0, count($value) - 1));
+ $opts = $arrayType ? array('class' => 'array') + $options : $options;
+ $value = $this->item($model, $value, compact('pathKey') + $opts);
+ } elseif ($field['array']) {
+ $opts = array('class' => 'array') + $options;
+ $value = $this->item($model, array($value), compact('pathKey') + $opts);
+ }
+ $data[$key] = $value;
+ }
+ return $options['first'] ? reset($data) : $data;
+ }
+
public function result($type, $resource, $context) {
switch ($type) {
case 'next':
@@ -45,10 +133,19 @@ class MockDocumentSource extends \lithium\data\Source {
unset($resource);
$result = null;
break;
-
}
return $result;
}
+
+ public function relationship($class, $type, $name, array $options = array()) {
+ $keys = Inflector::camelize($type == 'belongsTo' ? $name : $class::meta('name'));
+
+ $options += compact('name', 'type', 'keys');
+ $options['from'] = $class;
+
+ $relationship = $this->_classes['relationship'];
+ return new $relationship($options);
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/model/MockQueryComment.php b/libraries/lithium/tests/mocks/data/model/MockQueryComment.php
index a4a4553..6805db2 100644
--- a/libraries/lithium/tests/mocks/data/model/MockQueryComment.php
+++ b/libraries/lithium/tests/mocks/data/model/MockQueryComment.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -11,7 +11,8 @@ namespace lithium\tests\mocks\data\model;
class MockQueryComment extends \lithium\data\Model {
protected $_meta = array(
- 'source' => false
+ 'source' => false,
+ 'connection' => 'mock-database-connection'
);
protected $_schema = array(
diff --git a/libraries/lithium/tests/mocks/data/model/MockQueryPost.php b/libraries/lithium/tests/mocks/data/model/MockQueryPost.php
index fe3f0d7..e5ad8d1 100644
--- a/libraries/lithium/tests/mocks/data/model/MockQueryPost.php
+++ b/libraries/lithium/tests/mocks/data/model/MockQueryPost.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -11,7 +11,8 @@ namespace lithium\tests\mocks\data\model;
class MockQueryPost extends \lithium\data\Model {
protected $_meta = array(
- 'source' => false
+ 'source' => false,
+ 'connection' => 'mock-database-connection'
);
protected $_schema = array(
diff --git a/libraries/lithium/tests/mocks/data/model/mock_database/MockResult.php b/libraries/lithium/tests/mocks/data/model/mock_database/MockResult.php
new file mode 100644
index 0000000..e2f28a2
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/model/mock_database/MockResult.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\model\mock_database;
+
+class MockResult extends \lithium\data\source\database\Result {
+
+ public $records = array();
+
+ protected function _close() {
+ }
+
+ protected function _next() {
+ return next($this->records);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/Galleries.php b/libraries/lithium/tests/mocks/data/source/Galleries.php
new file mode 100644
index 0000000..b739fbf
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/source/Galleries.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\source;
+
+class Galleries extends \lithium\data\Model {
+
+ protected $_meta = array('connection' => 'test');
+
+ public $hasMany = array('Images');
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/Images.php b/libraries/lithium/tests/mocks/data/source/Images.php
new file mode 100644
index 0000000..068bdd6
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/source/Images.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\source;
+
+class Images extends \lithium\data\Model {
+
+ protected $_meta = array('connection' => 'test');
+
+ public $belongsTo = array('Galleries');
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/MockHttpModel.php b/libraries/lithium/tests/mocks/data/source/MockHttpModel.php
new file mode 100644
index 0000000..072d273
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/source/MockHttpModel.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\source;
+
+class MockHttpModel extends \lithium\data\Model {
+
+ protected $_meta = array(
+ 'source' => 'posts',
+ 'connection' => 'mock-http-connection'
+ );
+
+ protected $_schema = array(
+ 'id' => array('type' => 'integer', 'key' => 'primary'),
+ 'author_id' => array('type' => 'integer'),
+ 'title' => array('type' => 'string', 'length' => 255),
+ 'body' => array('type' => 'text'),
+ 'created' => array('type' => 'datetime'),
+ 'updated' => array('type' => 'datetime')
+ );
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/MockMongoConnection.php b/libraries/lithium/tests/mocks/data/source/MockMongoConnection.php
new file mode 100644
index 0000000..e45d02f
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/source/MockMongoConnection.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\source;
+
+class MockMongoConnection extends \lithium\data\source\MongoDb {
+
+ public function connect() {
+ return false;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/MockMongoPost.php b/libraries/lithium/tests/mocks/data/source/MockMongoPost.php
new file mode 100644
index 0000000..8535e25
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/source/MockMongoPost.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\source;
+
+use lithium\data\source\MongoDb;
+
+class MockMongoPost extends \lithium\data\Model {
+
+ protected $_meta = array(
+ 'connection' => 'lithium_mongo_test',
+ 'source' => 'posts'
+ );
+
+ protected $_connection;
+
+ protected $_useRealConnection = true;
+
+ public static function schema($field = null) {
+ if (is_array($field)) {
+ return static::_object()->_schema = $field;
+ }
+ return parent::schema($field);
+ }
+
+ public static function &connection() {
+ $self = static::_object();
+
+ if ($self->_useRealConnection) {
+ return parent::connection();
+ }
+ if (!$self->_connection) {
+ $self->_connection = new MongoDb(array('autoConnect' => false));
+ }
+ return $self->_connection;
+ }
+
+ public static function resetConnection($mock) {
+ $self = static::_object();
+ $self->_connection = null;
+ $self->_useRealConnection = !$mock;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/MockMongoSource.php b/libraries/lithium/tests/mocks/data/source/MockMongoSource.php
new file mode 100644
index 0000000..abf159b
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/source/MockMongoSource.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\source;
+
+use MongoId;
+use lithium\tests\mocks\data\source\mongo_db\MockResult;
+
+class MockMongoSource extends \lithium\core\Object {
+
+ public $resultSets = array();
+
+ public $queries = array();
+
+ public function __get($name) {
+ return $this;
+ }
+
+ public function insert(&$data, $options) {
+ $this->queries[] = compact('data', 'options');
+ $result = current($this->resultSets);
+ next($this->resultSets);
+ $data['_id'] = new MongoId();
+ return $result;
+ }
+
+ public function find($conditions, $fields) {
+ $this->queries[] = compact('conditions', 'fields');
+ $result = new MockResult(array('data' => current($this->resultSets)));
+ next($this->resultSets);
+ return $result;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/database/adapter/MockAdapter.php b/libraries/lithium/tests/mocks/data/source/database/adapter/MockAdapter.php
index e1b876a..edd3962 100644
--- a/libraries/lithium/tests/mocks/data/source/database/adapter/MockAdapter.php
+++ b/libraries/lithium/tests/mocks/data/source/database/adapter/MockAdapter.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -63,7 +63,7 @@ class MockAdapter extends \lithium\data\source\Database {
return $encoding ?: '';
}
- public function describe($entity, $meta = array()) {
+ public function describe($entity, array $meta = array()) {
return array();
}
diff --git a/libraries/lithium/tests/mocks/data/source/database/adapter/MockMySql.php b/libraries/lithium/tests/mocks/data/source/database/adapter/MockMySql.php
index 74a482d..d6e1d40 100644
--- a/libraries/lithium/tests/mocks/data/source/database/adapter/MockMySql.php
+++ b/libraries/lithium/tests/mocks/data/source/database/adapter/MockMySql.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD,http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD,http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/data/source/database/adapter/MockSqlite3.php b/libraries/lithium/tests/mocks/data/source/database/adapter/MockSqlite3.php
index 06f4442..74c0148 100644
--- a/libraries/lithium/tests/mocks/data/source/database/adapter/MockSqlite3.php
+++ b/libraries/lithium/tests/mocks/data/source/database/adapter/MockSqlite3.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/data/source/http/adapter/MockCouchPost.php b/libraries/lithium/tests/mocks/data/source/http/adapter/MockCouchPost.php
index 2a5c776..1c07cd2 100644
--- a/libraries/lithium/tests/mocks/data/source/http/adapter/MockCouchPost.php
+++ b/libraries/lithium/tests/mocks/data/source/http/adapter/MockCouchPost.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -12,6 +12,7 @@ class MockCouchPost extends \lithium\data\Model {
protected $_meta = array(
'source' => 'posts',
+ 'connection' => 'mock-couchdb-connection'
);
protected $_schema = array(
diff --git a/libraries/lithium/tests/mocks/data/source/http/adapter/MockService.php b/libraries/lithium/tests/mocks/data/source/http/adapter/MockService.php
deleted file mode 100644
index 57f8d56..0000000
--- a/libraries/lithium/tests/mocks/data/source/http/adapter/MockService.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\mocks\data\source\http\adapter;
-
-use \lithium\net\http\Response;
-
-class MockService extends \lithium\net\http\Service {
-
- public function send($method, $path = null, $data = array(), array $options = array()) {
- $defaults = array('return' => 'body', 'type' => 'form');
- $options += $defaults;
- $request = $this->_request($method, $path, $data, $options);
- $response = new Response();
-
- $response->body = json_encode(array(
- 'ok' => true,
- 'id' => '12345',
- 'rev' => '1-2',
- 'body' => 'something'
- ));
- $this->last = (object) compact('request', 'response');
- return ($options['return'] == 'body') ? $response->body() : $response;
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/data/source/http/adapter/MockSocket.php b/libraries/lithium/tests/mocks/data/source/http/adapter/MockSocket.php
index d5f9472..7b8fbea 100644
--- a/libraries/lithium/tests/mocks/data/source/http/adapter/MockSocket.php
+++ b/libraries/lithium/tests/mocks/data/source/http/adapter/MockSocket.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -25,13 +25,19 @@ class MockSocket extends \lithium\net\Socket {
}
public function read() {
+ $data = json_encode(array(
+ 'ok' => true,
+ 'id' => '12345',
+ 'rev' => '1-2',
+ 'body' => 'something'
+ ));
return join("\r\n", array(
'HTTP/1.1 200 OK',
'Header: Value',
'Connection: close',
'Content-Type: text/html;charset=UTF-8',
'',
- 'Test!'
+ $data
));
}
@@ -47,12 +53,9 @@ class MockSocket extends \lithium\net\Socket {
return true;
}
- public function send($message, array $options = array()) {
- if ($this->write($message)) {
- $message = $this->read();
- $response = new $options['classes']['response'](compact('message'));
- return $response;
- }
+ public function send($message = null, array $options = array()) {
+ $message = $this->read();
+ return new $options['classes']['response'](compact('message'));
}
}
diff --git a/libraries/lithium/tests/mocks/data/source/mongo_db/MockResult.php b/libraries/lithium/tests/mocks/data/source/mongo_db/MockResult.php
new file mode 100644
index 0000000..72799e2
--- /dev/null
+++ b/libraries/lithium/tests/mocks/data/source/mongo_db/MockResult.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\data\source\mongo_db;
+
+class MockResult extends \lithium\data\source\mongo_db\Result {
+
+ protected $_autoConfig = array('data');
+
+ protected $_data = array(
+ array('_id' => '4c8f86167675abfabdbf0300', 'title' => 'bar'),
+ array('_id' => '5c8f86167675abfabdbf0301', 'title' => 'foo'),
+ array('_id' => '6c8f86167675abfabdbf0302', 'title' => 'dib')
+ );
+
+ public function hasNext() {
+ if (!is_array($this->_data)) {
+ return false;
+ }
+ return key($this->_data) !== null && key($this->_data) < count($this->_data);
+ }
+
+ public function getNext() {
+ $result = current($this->_data);
+ next($this->_data);
+ return $result;
+ }
+
+ public function next() {
+ return $this->_next();
+ }
+
+ public function __call($method, array $params) {
+ return $this;
+ }
+
+ protected function _close() {
+ }
+
+ protected function _next() {
+ $result = current($this->_data) ?: null;
+ next($this->_data);
+ return $result;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/g11n/catalog/MockAdapter.php b/libraries/lithium/tests/mocks/g11n/catalog/MockAdapter.php
index 5105f3a..1a4159a 100644
--- a/libraries/lithium/tests/mocks/g11n/catalog/MockAdapter.php
+++ b/libraries/lithium/tests/mocks/g11n/catalog/MockAdapter.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/g11n/catalog/adapter/MockGettext.php b/libraries/lithium/tests/mocks/g11n/catalog/adapter/MockGettext.php
index 629385e..d3661d4 100644
--- a/libraries/lithium/tests/mocks/g11n/catalog/adapter/MockGettext.php
+++ b/libraries/lithium/tests/mocks/g11n/catalog/adapter/MockGettext.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/net/http/MockService.php b/libraries/lithium/tests/mocks/net/http/MockService.php
deleted file mode 100644
index dd4fd0f..0000000
--- a/libraries/lithium/tests/mocks/net/http/MockService.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\mocks\net\http;
-
-class MockService extends \lithium\net\http\Service {
-
- public function get($path = null, $data = array(), array $options = array()) {
- $defaults = array('type' => 'form', 'return' => 'body');
- $options += $defaults;
- if ($this->connect() === false) {
- return false;
- }
- $request = $this->_request('get', $path, $data, $options);
- $message = join("\r\n", array(
- 'HTTP/1.1 200 OK',
- 'Header: Value',
- 'Connection: close',
- 'Content-Type: text/html;charset=UTF-8',
- '',
- 'Test!'
- ));
- $response = new $this->_classes['response'](compact('message'));
- $this->last = (object) compact('request', 'response');
- return ($options['return'] == 'body') ? $response->body() : $response;
-
- }
-
- public function reset() {
- parent::reset();
- $isJson = (
- !empty($this->request->headers['Content-Type']) &&
- $this->request->headers['Content-Type'] == 'application/json'
- );
- if ($isJson) {
- $this->response->body = json_encode(array('some' => 'json'));
- }
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/net/http/MockSocket.php b/libraries/lithium/tests/mocks/net/http/MockSocket.php
index 3575872..61903eb 100644
--- a/libraries/lithium/tests/mocks/net/http/MockSocket.php
+++ b/libraries/lithium/tests/mocks/net/http/MockSocket.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -12,6 +12,13 @@ class MockSocket extends \lithium\net\Socket {
public $data = null;
+ public $configs = array();
+
+ public function __construct(array $config = array()) {
+ $this->configs[] = $config;
+ parent::__construct((array) $config);
+ }
+
public function open() {
return true;
}
@@ -25,26 +32,15 @@ class MockSocket extends \lithium\net\Socket {
}
public function read() {
- return join("\r\n", array(
- 'HTTP/1.1 200 OK',
- 'Header: Value',
- 'Connection: close',
- 'Content-Type: text/html;charset=UTF-8',
- '',
- 'Test!'
- ));
+ return $this->data;
}
public function write($data) {
- return $this->data = $data;
- }
-
- public function send($message, array $options = array()) {
- if ($this->write($message)) {
- $message = $this->read();
- $response = new $options['classes']['response'](compact('message'));
- return $response;
+ if (!is_object($data)) {
+ $data = $this->_instance($this->_classes['request'], (array) $data + $this->_config);
}
+ $this->data = $data;
+ return true;
}
public function timeout($time) {
@@ -54,6 +50,10 @@ class MockSocket extends \lithium\net\Socket {
public function encoding($charset) {
return true;
}
+
+ public function config() {
+ return $this->_config;
+ }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/net/http/Template.php b/libraries/lithium/tests/mocks/net/http/Template.php
new file mode 100644
index 0000000..263b65d
--- /dev/null
+++ b/libraries/lithium/tests/mocks/net/http/Template.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\net\http;
+
+class Template extends \lithium\core\Object {
+
+ public function __construct(array $config = array()) {
+ $config['response']->headers('Custom', 'Value');
+ }
+
+ public function render() {}
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/net/socket/MockCurl.php b/libraries/lithium/tests/mocks/net/socket/MockCurl.php
deleted file mode 100644
index 7b2260a..0000000
--- a/libraries/lithium/tests/mocks/net/socket/MockCurl.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\mocks\net\socket;
-
-class MockCurl extends \lithium\net\socket\Curl {
-
- public function resource() {
- return $this->_resource;
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/net/socket/MockStream.php b/libraries/lithium/tests/mocks/net/socket/MockStream.php
deleted file mode 100644
index 896f9cf..0000000
--- a/libraries/lithium/tests/mocks/net/socket/MockStream.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\mocks\net\socket;
-
-class MockStream extends \lithium\net\socket\Stream {
-
- public function resource() {
- return $this->_resource;
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/security/auth/adapter/MockHttp.php b/libraries/lithium/tests/mocks/security/auth/adapter/MockHttp.php
new file mode 100644
index 0000000..99fa802
--- /dev/null
+++ b/libraries/lithium/tests/mocks/security/auth/adapter/MockHttp.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\security\auth\adapter;
+
+class MockHttp extends \lithium\security\auth\adapter\Http {
+
+ public $headers = array();
+
+ protected function _writeHeader($string) {
+ $this->headers[] = $string;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/storage/cache/strategy/MockConfigurizer.php b/libraries/lithium/tests/mocks/storage/cache/strategy/MockConfigurizer.php
new file mode 100644
index 0000000..b90cf92
--- /dev/null
+++ b/libraries/lithium/tests/mocks/storage/cache/strategy/MockConfigurizer.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\storage\cache\strategy;
+
+/**
+ * Mock strategy.
+ * For testing strategies that need construct-time parameters.
+ */
+class MockConfigurizer extends \lithium\core\Object {
+
+ public static $parameters = array();
+
+ /**
+ * Constructor.
+ *
+ * @param array $config
+ * @return void
+ */
+ public function __construct(array $config = array()) {
+ static::$parameters = $config;
+ }
+
+ /**
+ * Write strategy method.
+ *
+ * @param mixed $data The data to be modified.
+ * @return string Modified data.
+ */
+ public static function write($data) {
+ return static::$parameters;
+ }
+
+ /**
+ * Read strategy method.
+ * Unserializes the passed data.
+ *
+ * @param string $data Data read.
+ * @return mixed Modified data.
+ */
+ public static function read($data) {
+ return static::$parameters;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/storage/cache/strategy/MockSerializer.php b/libraries/lithium/tests/mocks/storage/cache/strategy/MockSerializer.php
new file mode 100644
index 0000000..874e5ab
--- /dev/null
+++ b/libraries/lithium/tests/mocks/storage/cache/strategy/MockSerializer.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\storage\cache\strategy;
+
+/**
+ * Mock strategy.
+ * Simulates the 'Serializer' strategy.
+ */
+class MockSerializer extends \lithium\core\Object {
+
+ /**
+ * Write strategy method.
+ * Serializes the passed data.
+ *
+ * @link http://php.net/manual/en/function.serialize.php PHP Manual: serialize()
+ * @param mixed $data The data to be serialized.
+ * @return string Serialized data.
+ */
+ public function write($data) {
+ return serialize($data);
+ }
+
+ /**
+ * Read strategy method.
+ * Unserializes the passed data.
+ *
+ * @link http://php.net/manual/en/function.unserialize.php PHP Manual: unserialize()
+ * @param string $data Serialized data.
+ * @return mixed Result of unserialization.
+ */
+ public function read($data) {
+ return unserialize($data);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/storage/session/adapter/MockPhp.php b/libraries/lithium/tests/mocks/storage/session/adapter/MockPhp.php
new file mode 100644
index 0000000..2dbd733
--- /dev/null
+++ b/libraries/lithium/tests/mocks/storage/session/adapter/MockPhp.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace lithium\tests\mocks\storage\session\adapter;
+
+use lithium\storage\session\adapter\Php;
+
+class MockPhp extends Php {
+
+ /**
+ * Overriden method for testing.
+ *
+ * @return boolean false.
+ */
+ public static function isStarted() {
+ return false;
+ }
+
+ /**
+ * Overriden method for testing.
+ *
+ * @return boolean false.
+ */
+ protected static function _startup() {
+ return false;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/storage/session/adapter/SessionStorageConditional.php b/libraries/lithium/tests/mocks/storage/session/adapter/SessionStorageConditional.php
index 24412b0..f31aa76 100644
--- a/libraries/lithium/tests/mocks/storage/session/adapter/SessionStorageConditional.php
+++ b/libraries/lithium/tests/mocks/storage/session/adapter/SessionStorageConditional.php
@@ -2,7 +2,7 @@
namespace lithium\tests\mocks\storage\session\adapter;
-use \lithium\storage\session\adapter\Memory;
+use lithium\storage\session\adapter\Memory;
class SessionStorageConditional extends Memory {
diff --git a/libraries/lithium/tests/mocks/storage/session/strategy/MockCookieSession.php b/libraries/lithium/tests/mocks/storage/session/strategy/MockCookieSession.php
new file mode 100644
index 0000000..ba5e2b4
--- /dev/null
+++ b/libraries/lithium/tests/mocks/storage/session/strategy/MockCookieSession.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\storage\session\strategy;
+
+class MockCookieSession extends \lithium\core\Object {
+
+ protected static $_secret = 'foobar';
+
+ protected static $_data = array('one' => 'foo', 'two' => 'bar');
+
+ public static function read($key = null, array $options = array()) {
+ if (isset(static::$_data[$key])) {
+ return static::$_data[$key];
+ }
+ return static::$_data;
+ }
+
+ public static function write($key, $value = null, array $options = array()) {
+ static::$_data[$key] = $value;
+ return $value;
+ }
+
+ public static function reset() {
+ return static::$_data = array('one' => 'foo', 'two' => 'bar');
+ }
+
+ /**
+ * Method for returning data currently stored in this mock.
+ *
+ * @return array
+ */
+ public static function data() {
+ return static::$_data;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/template/MockHelper.php b/libraries/lithium/tests/mocks/template/MockHelper.php
index d67a5aa..35c9166 100644
--- a/libraries/lithium/tests/mocks/template/MockHelper.php
+++ b/libraries/lithium/tests/mocks/template/MockHelper.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -11,6 +11,7 @@ namespace lithium\tests\mocks\template;
class MockHelper extends \lithium\template\Helper {
protected $_strings = array('link' => '<a href="{:url}"{:options}>{:title}</a>');
+
/**
* Hack to expose protected properties for testing.
*
diff --git a/libraries/lithium/tests/mocks/template/MockRenderer.php b/libraries/lithium/tests/mocks/template/MockRenderer.php
index 1ce3282..5ac3f46 100644
--- a/libraries/lithium/tests/mocks/template/MockRenderer.php
+++ b/libraries/lithium/tests/mocks/template/MockRenderer.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/template/MockView.php b/libraries/lithium/tests/mocks/template/MockView.php
new file mode 100644
index 0000000..7609377
--- /dev/null
+++ b/libraries/lithium/tests/mocks/template/MockView.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\template;
+
+class MockView extends \lithium\template\View {
+
+ public function renderer() {
+ return $this->_config['renderer'];
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/template/helper/MockFormPost.php b/libraries/lithium/tests/mocks/template/helper/MockFormPost.php
index 29681ba..66ae96d 100644
--- a/libraries/lithium/tests/mocks/template/helper/MockFormPost.php
+++ b/libraries/lithium/tests/mocks/template/helper/MockFormPost.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/template/helper/MockFormRenderer.php b/libraries/lithium/tests/mocks/template/helper/MockFormRenderer.php
index 3f54fcd..97b25eb 100644
--- a/libraries/lithium/tests/mocks/template/helper/MockFormRenderer.php
+++ b/libraries/lithium/tests/mocks/template/helper/MockFormRenderer.php
@@ -2,13 +2,13 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\tests\mocks\template\helper;
-use \lithium\action\Request;
+use lithium\action\Request;
class MockFormRenderer extends \lithium\template\view\Renderer {
diff --git a/libraries/lithium/tests/mocks/template/helper/MockHtmlRenderer.php b/libraries/lithium/tests/mocks/template/helper/MockHtmlRenderer.php
index 2c093f2..8183fc2 100644
--- a/libraries/lithium/tests/mocks/template/helper/MockHtmlRenderer.php
+++ b/libraries/lithium/tests/mocks/template/helper/MockHtmlRenderer.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/test/MockFilterClass.php b/libraries/lithium/tests/mocks/test/MockFilterClass.php
new file mode 100644
index 0000000..71db405
--- /dev/null
+++ b/libraries/lithium/tests/mocks/test/MockFilterClass.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\test;
+
+class MockFilterClass extends \lithium\core\Object{
+ public function __construct($all = false) {
+ if($all) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function testFunction() {
+ $test = true;
+
+ return $test;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/test/MockFilterClassTest.php b/libraries/lithium/tests/mocks/test/MockFilterClassTest.php
new file mode 100644
index 0000000..a5f21af
--- /dev/null
+++ b/libraries/lithium/tests/mocks/test/MockFilterClassTest.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\test;
+
+use lithium\tests\mocks\test\MockFilterClass;
+
+class MockFilterClassTest extends \lithium\test\Unit {
+
+ public function testNothing() {
+ $coverage = new MockFilterClass();
+
+ $this->assertTrue(true);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/test/MockIntegrationTest.php b/libraries/lithium/tests/mocks/test/MockIntegrationTest.php
new file mode 100644
index 0000000..4d5dfba
--- /dev/null
+++ b/libraries/lithium/tests/mocks/test/MockIntegrationTest.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\test;
+
+class MockIntegrationTest extends \lithium\test\Integration {
+
+ public function testPass() {
+ $this->assertTrue(true);
+ }
+
+ public function testFail() {
+ $this->assertTrue(false);
+ }
+
+ public function testAnotherPass() {
+ $this->assertTrue(true);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/test/MockUnitTest.php b/libraries/lithium/tests/mocks/test/MockUnitTest.php
index 4720fc6..bf364bc 100644
--- a/libraries/lithium/tests/mocks/test/MockUnitTest.php
+++ b/libraries/lithium/tests/mocks/test/MockUnitTest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/test/cases/MockSkipThrowsException.php b/libraries/lithium/tests/mocks/test/cases/MockSkipThrowsException.php
new file mode 100644
index 0000000..013fa64
--- /dev/null
+++ b/libraries/lithium/tests/mocks/test/cases/MockSkipThrowsException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\test\cases;
+
+use Exception;
+
+class MockSkipThrowsException extends \lithium\test\Unit {
+
+ public function skip() {
+ throw new Exception('skip throws exception');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/test/cases/MockTest.php b/libraries/lithium/tests/mocks/test/cases/MockTest.php
index 024dc5a..9bce9dc 100644
--- a/libraries/lithium/tests/mocks/test/cases/MockTest.php
+++ b/libraries/lithium/tests/mocks/test/cases/MockTest.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/test/cases/MockTestErrorHandling.php b/libraries/lithium/tests/mocks/test/cases/MockTestErrorHandling.php
new file mode 100644
index 0000000..8ebd70c
--- /dev/null
+++ b/libraries/lithium/tests/mocks/test/cases/MockTestErrorHandling.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\test\cases;
+
+use Exception;
+
+class MockTestErrorHandling extends \lithium\test\Unit {
+
+ public function testNotEnoughParams() {
+ $this->_arrayPermute();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/test/reporter/MockHtml.php b/libraries/lithium/tests/mocks/test/reporter/MockHtml.php
deleted file mode 100644
index f72d68d..0000000
--- a/libraries/lithium/tests/mocks/test/reporter/MockHtml.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\mocks\test\reporter;
-
-class MockHtml extends \lithium\test\reporter\Html {
-
- public function result($stats) {
- return $this->_result($stats);
- }
-
- public function fail($fails) {
- return $this->_fail($fails);
- }
-
- public function exception($exceptions) {
- return $this->_exception($exceptions);
- }
-
- public function skip($skips) {
- return $this->_skip($skips);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/test/reporter/MockText.php b/libraries/lithium/tests/mocks/test/reporter/MockText.php
deleted file mode 100644
index 08a157f..0000000
--- a/libraries/lithium/tests/mocks/test/reporter/MockText.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-/**
- * Lithium: the most rad php framework
- *
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * @license http://opensource.org/licenses/bsd-license.php The BSD License
- */
-
-namespace lithium\tests\mocks\test\reporter;
-
-class MockText extends \lithium\test\reporter\Text {
-
- public function result($stats) {
- return $this->_result($stats);
- }
-
- public function fail($fails) {
- return $this->_fail($fails);
- }
-
- public function exception($exceptions) {
- return $this->_exception($exceptions);
- }
-
- public function skip($skips) {
- return $this->_skip($skips);
- }
-}
-
-?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/util/MockCollectionMarker.php b/libraries/lithium/tests/mocks/util/MockCollectionMarker.php
index f0c8155..e1798a0 100644
--- a/libraries/lithium/tests/mocks/util/MockCollectionMarker.php
+++ b/libraries/lithium/tests/mocks/util/MockCollectionMarker.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/util/MockCollectionObject.php b/libraries/lithium/tests/mocks/util/MockCollectionObject.php
index 7ef5406..8807257 100644
--- a/libraries/lithium/tests/mocks/util/MockCollectionObject.php
+++ b/libraries/lithium/tests/mocks/util/MockCollectionObject.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -12,8 +12,8 @@ class MockCollectionObject extends \lithium\core\Object {
public $data = array(1 => 2);
- public function invokeMethod($method, $params = array()) {
- return $method;
+ public function testFoo() {
+ return 'testFoo';
}
public function to($format, array $options = array()) {
diff --git a/libraries/lithium/tests/mocks/util/MockCollectionStringCast.php b/libraries/lithium/tests/mocks/util/MockCollectionStringCast.php
index 5af3cc2..0dd9b58 100644
--- a/libraries/lithium/tests/mocks/util/MockCollectionStringCast.php
+++ b/libraries/lithium/tests/mocks/util/MockCollectionStringCast.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/tests/mocks/util/MockFilters.php b/libraries/lithium/tests/mocks/util/MockFilters.php
new file mode 100644
index 0000000..d13d8d3
--- /dev/null
+++ b/libraries/lithium/tests/mocks/util/MockFilters.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\mocks\util;
+
+class MockFilters extends \lithium\core\StaticObject {
+
+ public static function filteredMethod() {
+ return static::_filter(__FUNCTION__, array(), function($self, $params) {
+ return 'Working?';
+ });
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/mocks/util/MockStringObject.php b/libraries/lithium/tests/mocks/util/MockStringObject.php
index 510efd2..e093c67 100644
--- a/libraries/lithium/tests/mocks/util/MockStringObject.php
+++ b/libraries/lithium/tests/mocks/util/MockStringObject.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
diff --git a/libraries/lithium/util/Collection.php b/libraries/lithium/util/Collection.php
index f0e7281..ddabf61 100644
--- a/libraries/lithium/util/Collection.php
+++ b/libraries/lithium/util/Collection.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\util;
/**
- * The parent class for all collection objects. Contains methods for collection iteration,
+ * The parent class for all collection objects. Contains methods for collection iteration,
* conversion, and filtering. Implements `ArrayAccess`, `Iterator`, and `Countable`.
*
* Collection objects can act very much like arrays. This is especially evident in creating new
@@ -20,9 +20,9 @@ namespace lithium\util;
* $coll[] = 'foo';
* // $coll[0] --> 'foo'
*
- * $coll = new Collection(array('items' => array('foo')));
+ * $coll = new Collection(array('data' => array('foo')));
* // $coll[0] --> 'foo'
- *
+ *
* $array = $coll->to('array');
* }}}
*
@@ -30,7 +30,7 @@ namespace lithium\util;
*
* {{{
*
- * $coll = new Collection(array('items' => array(0, 1, 2, 3, 4)));
+ * $coll = new Collection(array('data' => array(0, 1, 2, 3, 4)));
*
* $coll->first(); // 1 (the first non-empty value)
* $coll->current(); // 0
@@ -56,12 +56,12 @@ namespace lithium\util;
* }
* }
*
- * $items = array(
+ * $data = array(
* new Task(array('task' => 'task 1')),
* new Task(array('task' => 'task 2')),
* new Task(array('task' => 'task 3'))
* );
- * $tasks = new Collection(compact('items'));
+ * $tasks = new Collection(compact('data'));
*
* // $result will contain an array, and each element will be the return
* // value of a run() method call:
@@ -71,9 +71,9 @@ namespace lithium\util;
* $result = $tasks->run('now');
* }}}
*
- * @link http://us.php.net/manual/en/class.arrayaccess.php
- * @link http://us.php.net/manual/en/class.iterator.php
- * @link http://us.php.net/manual/en/class.countable.php
+ * @link http://us.php.net/manual/en/class.arrayaccess.php PHP Manual: ArrayAccess Interface
+ * @link http://us.php.net/manual/en/class.iterator.php PHP Manual: Iterator Interface
+ * @link http://us.php.net/manual/en/class.countable.php PHP Manual: Countable Interface
*/
class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator, \Countable {
@@ -85,7 +85,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @var array
*/
protected static $_formats = array(
- 'array' => '\lithium\util\Collection::_toArray'
+ 'array' => 'lithium\util\Collection::toArray'
);
/**
@@ -93,7 +93,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
*
* @var array
*/
- protected $_items = array();
+ protected $_data = array();
/**
* Indicates whether the current position is valid or not.
@@ -108,7 +108,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
*
* @var array
*/
- protected $_autoConfig = array('items');
+ protected $_autoConfig = array('data');
/**
* Accessor method for adding format handlers to instances and subclasses of `Collection`.
@@ -159,7 +159,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
*/
public static function formats($format, $handler = null) {
if ($format === false) {
- return static::$_formats = array('array' => '\lithium\util\Collection::_toArray');
+ return static::$_formats = array('array' => '\lithium\util\Collection::toArray');
}
if ((is_null($handler)) && class_exists($format)) {
return static::$_formats[] = $format;
@@ -175,52 +175,35 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
*/
protected function _init() {
parent::_init();
- unset($this->_config['items']);
+ unset($this->_config['data']);
}
/**
* Handles dispatching of methods against all items in the collection.
*
- * @param string $method
- * @param array $parameters
+ * @param string $method The name of the method to call on each instance in the collection.
+ * @param array $params The parameters to pass on each method call.
* @param array $options Specifies options for how to run the given method against the object
* collection. The available options are:
* - `'collect'`: If `true`, the results of this method call will be returned
- * wrapped in a new Collection object or subclass.
+ * wrapped in a new `Collection` object or subclass.
* - `'merge'`: Used primarily if the method being invoked returns an array. If
* set to `true`, merges all results arrays into one.
* @todo Implement filtering.
- * @return mixed
+ * @return mixed Returns either an array of the return values of the methods, or the return
+ * values wrapped in a `Collection` instance.
*/
- public function invoke($method, $parameters = array(), array $options = array()) {
+ public function invoke($method, array $params = array(), array $options = array()) {
+ $class = get_class($this);
$defaults = array('merge' => false, 'collect' => false);
$options += $defaults;
- $results = array();
- $isCore = null;
-
- foreach ($this->_items as $key => $value) {
- if (is_null($isCore)) {
- $isCore = (method_exists(current($this->_items), 'invokeMethod'));
- }
-
- if ($isCore) {
- $result = $this->_items[$key]->invokeMethod($method, $parameters);
- } else {
- $result = call_user_func_array(array(&$this->_items[$key], $method), $parameters);
- }
-
- if (!empty($options['merge'])) {
- $results = array_merge($results, $result);
- } else {
- $results[$key] = $result;
- }
- }
+ $data = array();
- if ($options['collect']) {
- $class = get_class($this);
- $results = new $class(array('items' => $results));
+ foreach ($this as $object) {
+ $value = call_user_func_array(array(&$object, $method), $params);
+ ($options['merge']) ? $data = array_merge($data, $value) : $data[$this->key()] = $value;
}
- return $results;
+ return ($options['collect']) ? new $class(compact('data')) : $data;
}
/**
@@ -264,7 +247,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
public function to($format, array $options = array()) {
$defaults = array('internal' => false);
$options += $defaults;
- $data = $options['internal'] ? $this->_items : $this;
+ $data = $options['internal'] ? $this->_data : $this;
if (is_object($format) && is_callable($format)) {
return $format($data, $options);
@@ -297,40 +280,40 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @param callback $filter Callback to use for filtering.
* @param array $options The available options are:
* - `'collect'`: If `true`, the results will be returned wrapped
- * in a new Collection object or subclass.
+ * in a new `Collection` object or subclass.
* @return array|object The filtered items.
*/
public function find($filter, array $options = array()) {
$defaults = array('collect' => true);
$options += $defaults;
- $items = array_filter($this->_items, $filter);
+ $data = array_filter($this->_data, $filter);
if ($options['collect']) {
$class = get_class($this);
- $items = new $class(compact('items'));
+ $data = new $class(compact('data'));
}
- return $items;
+ return $data;
}
/**
* Returns the first non-empty value in the collection after a filter is applied, or rewinds the
* collection and returns the first value.
*
+ * @see lithium\util\Collection::rewind()
* @param callback $filter A closure through which collection values will be
* passed. If the return value of this function is non-empty,
* it will be returned as the result of the method call. If `null`, the
* collection is rewound (see `rewind()`) and the first item is returned.
* @return mixed Returns the first non-empty collection value returned from `$filter`.
- * @see lithium\util\Collection::rewind()
*/
public function first($filter = null) {
- if (empty($filter)) {
+ if (!$filter) {
return $this->rewind();
}
- foreach ($this->_items as $item) {
- if ($value = $filter($item)) {
- return $value;
+ foreach ($this as $item) {
+ if ($filter($item)) {
+ return $item;
}
}
}
@@ -342,30 +325,30 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return object This collection instance.
*/
public function each($filter) {
- $this->_items = array_map($filter, $this->_items);
+ $this->_data = array_map($filter, $this->_data);
return $this;
}
/**
- * Applies a callback to a copy of all items in the collection
+ * Applies a callback to a copy of all data in the collection
* and returns the result.
*
* @param callback $filter The filter to apply.
* @param array $options The available options are:
* - `'collect'`: If `true`, the results will be returned wrapped
- * in a new Collection object or subclass.
- * @return array|object The filtered items.
+ * in a new `Collection` object or subclass.
+ * @return array|object The filtered data.
*/
public function map($filter, array $options = array()) {
$defaults = array('collect' => true);
$options += $defaults;
- $items = array_map($filter, $this->_items);
+ $data = array_map($filter, $this->_data);
if ($options['collect']) {
$class = get_class($this);
- return new $class(compact('items'));
+ return new $class(compact('data'));
}
- return $items;
+ return $data;
}
/**
@@ -375,7 +358,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return boolean `true` if offset exists, `false` otherwise.
*/
public function offsetExists($offset) {
- return isset($this->_items[$offset]);
+ return isset($this->_data[$offset]);
}
/**
@@ -385,7 +368,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return mixed Value at offset.
*/
public function offsetGet($offset) {
- return $this->_items[$offset];
+ return $this->_data[$offset];
}
/**
@@ -397,9 +380,9 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
*/
public function offsetSet($offset, $value) {
if (is_null($offset)) {
- return $this->_items[] = $value;
+ return $this->_data[] = $value;
}
- return $this->_items[$offset] = $value;
+ return $this->_data[$offset] = $value;
}
/**
@@ -409,7 +392,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return void
*/
public function offsetUnset($offset) {
- unset($this->_items[$offset]);
+ unset($this->_data[$offset]);
}
/**
@@ -418,8 +401,8 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return mixed The current item after rewinding.
*/
public function rewind() {
- $this->_valid = (reset($this->_items) !== false);
- return current($this->_items);
+ $this->_valid = (reset($this->_data) !== false);
+ return current($this->_data);
}
/**
@@ -428,8 +411,8 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return mixed The current item after moving.
*/
public function end() {
- $this->_valid = (end($this->_items) !== false);
- return current($this->_items);
+ $this->_valid = (end($this->_data) !== false);
+ return current($this->_data);
}
/**
@@ -447,7 +430,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return mixed The current item.
*/
public function current() {
- return current($this->_items);
+ return current($this->_data);
}
/**
@@ -456,7 +439,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return scalar Scalar on success `0` on failure.
*/
public function key() {
- return key($this->_items);
+ return key($this->_data);
}
/**
@@ -466,10 +449,10 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return mixed The current item after moving.
*/
public function prev() {
- if (!prev($this->_items)) {
- end($this->_items);
+ if (!prev($this->_data)) {
+ end($this->_data);
}
- return current($this->_items);
+ return current($this->_data);
}
/**
@@ -478,8 +461,8 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return The current item after moving.
*/
public function next() {
- $this->_valid = (next($this->_items) !== false);
- return current($this->_items);
+ $this->_valid = (next($this->_data) !== false);
+ return current($this->_data);
}
/**
@@ -489,13 +472,13 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return void
*/
public function append($value) {
- is_object($value) ? $this->_items[] =& $value : $this->_items[] = $value;
+ is_object($value) ? $this->_data[] =& $value : $this->_data[] = $value;
}
/**
* Counts the items of the object.
*
- * @return integer Number of items.
+ * @return integer Returns the number of items in the collection.
*/
public function count() {
$count = iterator_count($this);
@@ -509,7 +492,7 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
* @return array The keys of the items.
*/
public function keys() {
- return array_keys($this->_items);
+ return array_keys($this->_data);
}
/**
@@ -517,10 +500,16 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
*
* @param mixed $data Either a `Collection` instance, or an array representing a `Collection`'s
* internal state.
+ * @param array $options Options used when converting `$data` to an array:
+ * - `'handlers'` _array_: An array where the keys are fully-namespaced class
+ * names, and the values are closures that take an instance of the class as a
+ * parameter, and return an array or scalar value that the instance represents.
* @return array Returns the value of `$data` as a pure PHP array, recursively converting all
* sub-objects and other values to their closest array or scalar equivalents.
*/
- protected static function _toArray($data) {
+ public static function toArray($data, array $options = array()) {
+ $defaults = array('handlers' => array());
+ $options += $defaults;
$result = array();
foreach ($data as $key => $item) {
@@ -528,6 +517,9 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
case (!is_object($item)):
$result[$key] = $item;
break;
+ case (isset($options['handlers'][$class = get_class($item)])):
+ $result[$key] = $options['handlers'][$class]($item);
+ break;
case (method_exists($item, 'to')):
$result[$key] = $item->to('array');
break;
@@ -537,6 +529,9 @@ class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator
case (method_exists($item, '__toString')):
$result[$key] = (string) $item;
break;
+ default:
+ $result[$key] = $item;
+ break;
}
}
return $result;
diff --git a/libraries/lithium/util/Inflector.php b/libraries/lithium/util/Inflector.php
index d5ed407..1b33aa4 100644
--- a/libraries/lithium/util/Inflector.php
+++ b/libraries/lithium/util/Inflector.php
@@ -2,13 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* Copyright 2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @license http://opensource.org/licenses/mit-license.php The MIT License
*/
namespace lithium\util;
+
/**
* Utility for modifying format of words. Change singular to plural and vice versa.
* Under_score a CamelCased word and vice versa. Replace spaces and special characters.
@@ -26,9 +27,16 @@ class Inflector {
* @var array
*/
protected static $_transliteration = array(
- '/à |á|Ã¥|â/' => 'a', '/è|é|ê|ẽ|ë/' => 'e', '/ì|Ã|î/' => 'i', '/ò|ó|ô|ø/' => 'o',
- '/ù|ú|ů|û/' => 'u', '/ç/' => 'c', '/ñ/' => 'n', '/ä|æ/' => 'ae', '/ö/' => 'oe',
- '/ü/' => 'ue', '/Ä/' => 'Ae', '/Ü/' => 'Ue', '/Ö/' => 'Oe', '/ß/' => 'ss'
+ '/à |á|å|â/' => 'a',
+ '/è|é|ê|ẽ|ë/' => 'e',
+ '/ì|Ã|î/' => 'i',
+ '/ò|ó|ô|ø/' => 'o',
+ '/ù|ú|ů|û/' => 'u',
+ '/ç/' => 'c', '/ñ/' => 'n',
+ '/ä|æ/' => 'ae', '/ö/' => 'oe',
+ '/ü/' => 'ue', '/Ä/' => 'Ae',
+ '/Ü/' => 'Ue', '/Ö/' => 'Oe',
+ '/ß/' => 'ss'
);
/**
@@ -189,7 +197,7 @@ class Inflector {
protected static $_underscored = array();
/**
- * Contains a cache map of previously humanize words.
+ * Contains a cache map of previously humanized words.
*
* @var array
*/
@@ -307,7 +315,7 @@ class Inflector {
$regexIrregular = static::_enclose(join('|', array_keys($irregular)));
static::$_singular += compact('regexUninflected', 'regexIrregular');
}
- if (preg_match('/(.*)\\b(' . $regexIrregular . ')$/i', $word, $regs)) {
+ if (preg_match("/(.*)\\b({$regexIrregular})\$/i", $word, $regs)) {
$singular = substr($word, 0, 1) . substr($irregular[strtolower($regs[2])], 1);
return static::$_singularized[$word] = $regs[1] . $singular;
}
@@ -331,28 +339,40 @@ class Inflector {
public static function reset() {
static::$_singularized = static::$_pluralized = array();
static::$_camelized = static::$_underscored = array();
+ static::$_humanized = array();
+
static::$_plural['regexUninflected'] = static::$_singular['regexUninflected'] = null;
static::$_plural['regexIrregular'] = static::$_singular['regexIrregular'] = null;
+ static::$_transliteration = array(
+ '/à |á|å|â/' => 'a', '/è|é|ê|ẽ|ë/' => 'e',
+ '/ì|Ã|î/' => 'i', '/ò|ó|ô|ø/' => 'o',
+ '/ù|ú|ů|û/' => 'u', '/ç/' => 'c',
+ '/ñ/' => 'n', '/ä|æ/' => 'ae', '/ö/' => 'oe',
+ '/ü/' => 'ue', '/Ä/' => 'Ae',
+ '/Ü/' => 'Ue', '/Ö/' => 'Oe',
+ '/ß/' => 'ss'
+ );
}
/**
* Takes a under_scored word and turns it into a CamelCased or camelBack word
*
- * @param string $word Under_scored version of a word (i.e. `'red_bike'`).
+ * @param string $word An under_scored or slugged word (i.e. `'red_bike'` or `'red-bike'`).
* @param boolean $cased If false, first character is not upper cased
* @return string CamelCased version of the word (i.e. `'RedBike'`).
*/
public static function camelize($word, $cased = true) {
- if (isset(static::$_camelized[$word])) {
- return static::$_camelized[$word];
+ $_word = $word;
+
+ if (isset(static::$_camelized[$_word]) && $cased) {
+ return static::$_camelized[$_word];
}
- $word = str_replace(" ", "", ucwords(str_replace("_", " ", $word)));
+ $word = str_replace(" ", "", ucwords(str_replace(array("_", '-'), " ", $word)));
if (!$cased) {
- $replace = strtolower(substr($word, 0, 1));
- return preg_replace('/\\w/', $replace, $word, 1);
+ return lcfirst($word);
}
- return static::$_camelized[$word] = $word;
+ return static::$_camelized[$_word] = $word;
}
/**
@@ -365,9 +385,26 @@ class Inflector {
if (isset(static::$_underscored[$word])) {
return static::$_underscored[$word];
}
- return static::$_underscored[$word] = strtolower(
- preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $word)
+ return static::$_underscored[$word] = strtolower(static::slug($word, '_'));
+ }
+
+ /**
+ * Returns a string with all spaces converted to given replacement and
+ * non word characters removed. Maps special characters to ASCII using
+ * `Inflector::$_transliteration`, which can be updated using `Inflector::rules()`.
+ *
+ * @see lithium\util\Inflector::rules()
+ * @param string $string An arbitrary string to convert.
+ * @param string $replacement The replacement to use for spaces.
+ * @return string The converted string.
+ */
+ public static function slug($string, $replacement = '-') {
+ $map = static::$_transliteration + array(
+ '/[^\w\s]/' => ' ', '/\\s+/' => $replacement,
+ '/(?<=[a-z])([A-Z])/' => $replacement . '\\1',
+ str_replace(':rep', preg_quote($replacement, '/'), '/^[:rep]+|[:rep]+$/') => '',
);
+ return preg_replace(array_keys($map), array_values($map), $string);
}
/**
@@ -379,11 +416,10 @@ class Inflector {
* @return string Human readable version of the word (i.e. `'Red Bike'`).
*/
public static function humanize($word, $separator = '_') {
- if (isset(static::$_humanized[$word . $separator])) {
- return static::$_humanized[$word . $separator];
+ if (isset(static::$_humanized[$key = $word . ':' . $separator])) {
+ return static::$_humanized[$key];
}
- static::$_humanized[$word . $separator] = ucwords(str_replace($separator, " ", $word));
- return static::$_humanized[$word . $separator];
+ return static::$_humanized[$key] = ucwords(str_replace($separator, " ", $word));
}
/**
@@ -407,24 +443,6 @@ class Inflector {
}
/**
- * Returns a string with all spaces converted to given replacement and
- * non word characters removed. Maps special characters to ASCII using
- * `Inflector::$_transliteration`, which can be updated using `Inflector::rules()`.
- *
- * @see lithium\util\Inflector::rules()
- * @param string $string An arbitrary string to convert.
- * @param string $replacement The replacement to use for spaces.
- * @return string The converted string.
- */
- public static function slug($string, $replacement = '-') {
- $map = static::$_transliteration + array(
- '/[^\w\s]/' => ' ', '/\\s+/' => $replacement,
- str_replace(':rep', preg_quote($replacement, '/'), '/^[:rep]+|[:rep]+$/') => '',
- );
- return preg_replace(array_keys($map), array_values($map), $string);
- }
-
- /**
* Enclose a string for preg matching.
*
* @param string $string String to enclose
diff --git a/libraries/lithium/util/Set.php b/libraries/lithium/util/Set.php
index 179ace5..c804055 100644
--- a/libraries/lithium/util/Set.php
+++ b/libraries/lithium/util/Set.php
@@ -2,9 +2,8 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
- * Copyright 2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
- * @license http://opensource.org/licenses/mit-license.php The MIT License
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\util;
@@ -12,7 +11,7 @@ namespace lithium\util;
/**
* Used for complex manipulation, comparison, and access of array data. Some methods allow for
* XPath-like data access, as follows:
- *
+ *
* - `'/User/id'`: Similar to the classic {n}.User.id.
* - `'/User[2]/name'`: Selects the name of the second User.
* - `'/User[id>2]'`: Selects all Users with an id > 2.
@@ -33,39 +32,37 @@ class Set {
* Add the keys/values in `$array2` that are not found in `$array` onto the end of `$array`.
*
* @param mixed $array Original array.
- * @param mixed $array2 Second array to add onto the Original.
- * @return array A Blended array of keys and values.
+ * @param mixed $array2 Second array to add onto the original.
+ * @return array An array containing all the keys of the second array not already present in the
+ * first.
*/
- public static function blend($array, $array2) {
- if (empty($array) && !empty($array2)) {
+ public static function append(array $array, array $array2) {
+ if (!$array && $array2) {
return $array2;
}
- if (!empty($array) && !empty($array2)) {
- foreach ($array2 as $key => $value) {
- if (!isset($array[$key])) {
- $array[$key] = $value;
- } elseif (is_array($value)) {
- $array[$key] = static::blend($array[$key], $array2[$key]);
- }
+ foreach ($array2 as $key => $value) {
+ if (!isset($array[$key])) {
+ $array[$key] = $value;
+ } elseif (is_array($value)) {
+ $array[$key] = static::append($array[$key], $array2[$key]);
}
}
return $array;
}
/**
- * Checks if a particular path is set in an array
+ * Checks if a particular path is set in an array.
*
* @param mixed $data Data to check on.
* @param mixed $path A dot-delimited string.
* @return boolean `true` if path is found, `false` otherwise.
*/
public static function check($data, $path = null) {
- if (empty($path)) {
+ if (!$path) {
return $data;
}
- if (!is_array($path)) {
- $path = explode('.', $path);
- }
+ $path = is_array($path) ? $path : explode('.', $path);
+
foreach ($path as $i => $key) {
if (is_numeric($key) && intval($key) > 0 || $key === '0') {
$key = intval($key);
@@ -79,7 +76,6 @@ class Set {
$data =& $data[$key];
}
}
- return true;
}
/**
@@ -95,7 +91,7 @@ class Set {
* @return array Combined array.
*/
public static function combine($data, $path1 = null, $path2 = null, $groupPath = null) {
- if (empty($data)) {
+ if (!$data) {
return array();
}
if (is_object($data)) {
@@ -140,20 +136,23 @@ class Set {
}
/**
- * Determines if `val2` is contained in `val1`
+ * Determines if the array elements in `$array2` are wholly contained within `$array1`. Works
+ * recursively.
*
- * @param array $val1 First value.
- * @param array $val2 Second value.
- * @return boolean true if `$val1` contains `$val2`, `false` otherwise.
+ * @param array $array1 First value.
+ * @param array $array2 Second value.
+ * @return boolean Returns `true` if `$array1` wholly contains the keys and values of `$array2`,
+ * otherwise, returns `false`. Returns `false` if either array is empty.
*/
- public static function contains($val1, $val2) {
- if (empty($val1) || empty($val2)) {
+ public static function contains(array $array1, array $array2) {
+ if (!$array1 || !$array2) {
return false;
}
- foreach ((array) $val2 as $key => $val) {
- if (is_numeric($key)) {
- static::contains($val, $val1);
- } elseif (!isset($val1[$key]) || $val1[$key] != $val) {
+ foreach ($array2 as $key => $val) {
+ if (!isset($array1[$key]) || $array1[$key] != $val) {
+ return false;
+ }
+ if (is_array($val) && !static::contains($array1[$key], $val)) {
return false;
}
}
@@ -166,58 +165,49 @@ class Set {
*
* @param array $data Array to count dimensions on.
* @param array $options
- * @param integer $count Start the depth count at this number.
* @return integer The number of dimensions in `$array`.
*/
- public static function depth($data, $options = array(), $count = 0) {
- if (empty($data)) {
+ public static function depth($data, array $options = array()) {
+ $defaults = array('all' => false, 'count' => 0);
+ $options += $defaults;
+
+ if (!$data) {
return 0;
}
- if (!is_array($options)) {
- $options = array('all' => $options, 'count' => $count);
+
+ if (!$options['all']) {
+ return (is_array(reset($data))) ? static::depth(reset($data)) + 1 : 1;
}
- $defaults = array('all' => false, 'count' => 0);
- $options += $defaults;
+ $depth = array($options['count']);
- if (!empty($options['all'])) {
- $depth = array($options['count']);
- if (is_array($data) && reset($data) !== false) {
- foreach ($data as $value) {
- $depth[] = static::depth($value, array(
- 'all' => $options['all'],
- 'count' => $options['count'] + 1
- ));
- }
+ if (is_array($data) && reset($data) !== false) {
+ foreach ($data as $value) {
+ $depth[] = static::depth($value, array(
+ 'all' => $options['all'],
+ 'count' => $options['count'] + 1
+ ));
}
- return max($depth);
- }
- if (is_array(reset($data))) {
- return static::depth(reset($data)) + 1;
}
- return 1;
+ return max($depth);
}
/**
* Computes the difference between two arrays.
*
- * @param mixed $val1 First value.
- * @param mixed $val2 Second value.
+ * @param array $val1 First value.
+ * @param array $val2 Second value.
* @return array Computed difference.
*/
- public static function diff($val1, $val2 = null) {
- if (empty($val1)) {
- return (array) $val2;
- } elseif (empty($val2)) {
- return (array) $val1;
+ public static function diff(array $val1, array $val2) {
+ if (!$val1 || !$val2) {
+ return $val2 ?: $val1;
}
$out = array();
foreach ($val1 as $key => $val) {
$exists = isset($val2[$key]);
- if ($exists && $val2[$key] != $val) {
- $out[$key] = $val;
- } elseif (!$exists) {
+ if (($exists && $val2[$key] != $val) || !$exists) {
$out[$key] = $val;
}
unset($val2[$key]);
@@ -254,23 +244,26 @@ class Set {
* disabled for higher XPath-ness.
* @return array An array of matched items.
*/
- public static function extract($data, $path = null, array $options = array()) {
- if (empty($data)) {
+ public static function extract(array $data, $path = null, array $options = array()) {
+ if (!$data) {
return array();
}
+
if (is_string($data)) {
$tmp = $path;
$path = $data;
$data = $tmp;
unset($tmp);
}
+
if ($path === '/') {
return array_filter($data, function($data) {
return ($data === 0 || $data === '0' || !empty($data));
});
}
$contexts = $data;
- $options = array_merge(array('flatten' => true), $options);
+ $defaults = array('flatten' => true);
+ $options += $defaults;
if (!isset($contexts[0])) {
$contexts = array($data);
@@ -280,11 +273,13 @@ class Set {
do {
$token = array_shift($tokens);
$conditions = false;
+
if (preg_match_all('/\[([^=]+=\/[^\/]+\/|[^\]]+)\]/', $token, $m)) {
$conditions = $m[1];
$token = substr($token, 0, strpos($token, '['));
}
$matches = array();
+
foreach ($contexts as $key => $context) {
if (!isset($context['trace'])) {
$context = array('trace' => array(null), 'item' => $context, 'key' => $key);
@@ -294,7 +289,7 @@ class Set {
$context['trace'][] = $context['key'];
}
$parent = join('/', $context['trace']) . '/.';
- $context['item'] = static::extract($parent, $data);
+ $context['item'] = static::extract($data, $parent);
$context['key'] = array_pop($context['trace']);
if (isset($context['trace'][1]) && $context['trace'][1] > 0) {
$context['item'] = $context['item'][0];
@@ -351,7 +346,9 @@ class Set {
'item' => $item,
);
}
- } elseif (($key === $token || (ctype_digit($token) && $key == $token) || $token === '.')) {
+ } elseif (
+ ($key === $token || (ctype_digit($token) && $key == $token) || $token === '.')
+ ) {
$context['trace'][] = $key;
$matches[] = array(
'trace' => $context['trace'],
@@ -364,8 +361,9 @@ class Set {
foreach ($conditions as $condition) {
$filtered = array();
$length = count($matches);
+
foreach ($matches as $i => $match) {
- if (static::matches(array($condition), $match['item'], $i + 1, $length)) {
+ if (static::matches($match['item'], array($condition), $i + 1, $length)) {
$filtered[] = $match;
}
}
@@ -402,27 +400,21 @@ class Set {
* - `'path'`: Starting point (defaults to null).
* @return array
*/
- public static function flatten($data, $options = array()) {
- $result = array();
-
- if (!is_array($options)) {
- $options = array('separator' => $options);
- }
+ public static function flatten($data, array $options = array()) {
$defaults = array('separator' => '.', 'path' => null);
$options += $defaults;
+ $result = array();
if (!is_null($options['path'])) {
$options['path'] .= $options['separator'];
}
foreach ($data as $key => $val) {
- if (is_array($val)) {
- $result += (array) static::flatten($val, array(
- 'separator' => $options['separator'],
- 'path' => $options['path'] . $key
- ));
- } else {
+ if (!is_array($val)) {
$result[$options['path'] . $key] = $val;
+ continue;
}
+ $opts = array('separator' => $options['separator'], 'path' => $options['path'] . $key);
+ $result += (array) static::flatten($val, $opts);
}
return $result;
}
@@ -467,17 +459,19 @@ class Set {
}
$out[] = $formatted;
}
- } else {
- $count2 = count($data);
- for ($j = 0; $j < $count; $j++) {
- $args = array();
- for ($i = 0; $i < $count2; $i++) {
- if (isset($data[$i][$j])) {
- $args[] = $data[$i][$j];
- }
+ return $out;
+ }
+ $count2 = count($data);
+
+ for ($j = 0; $j < $count; $j++) {
+ $args = array();
+
+ for ($i = 0; $i < $count2; $i++) {
+ if (isset($data[$i][$j])) {
+ $args[] = $data[$i][$j];
}
- $out[] = vsprintf($format, $args);
}
+ $out[] = vsprintf($format, $args);
}
return $out;
}
@@ -490,7 +484,7 @@ class Set {
* @param array $data Data to insert.
* @return array
*/
- public static function insert($list, $path, $data = null) {
+ public static function insert($list, $path, $data = array()) {
if (!is_array($path)) {
$path = explode('.', $path);
}
@@ -542,18 +536,18 @@ class Set {
* This function can be used to see if a single item or a given XPath
* match certain conditions.
*
- * @param mixed $conditions An array of condition strings or an XPath expression.
* @param array $data An array of data to execute the match on.
+ * @param mixed $conditions An array of condition strings or an XPath expression.
* @param integer $i Optional: The 'nth'-number of the item being matched.
* @param integer $length
* @return boolean
*/
- public static function matches($conditions, $data = array(), $i = null, $length = null) {
- if (empty($conditions)) {
+ public static function matches($data = array(), $conditions, $i = null, $length = null) {
+ if (!$conditions) {
return true;
}
- if (is_string($conditions) || is_string($data)) {
- return !!static::extract($data, $conditions);
+ if (is_string($conditions)) {
+ return (boolean) static::extract($data, $conditions);
}
foreach ($conditions as $condition) {
if ($condition === ':last') {
@@ -614,16 +608,15 @@ class Set {
* with an unlimited amount of arguments and typecasts non-array parameters
* into arrays.
*
+ * @param array $array1 The base array.
+ * @param array $array2 The array to be merged on top of the base array.
* @return array Merged array of all passed params.
*/
- public static function merge($arr1, $arr2 = null) {
- $args = array($arr1, $arr2);
+ public static function merge(array $array1, array $array2) {
+ $args = array($array1, $array2);
- if (empty($arr1) && empty($arr2)) {
- return array();
- }
- if (empty($arr1) || empty($arr2)) {
- return empty($arr1) ? (array) $arr2 : (array) $arr1;
+ if (!$array1 || !$array2) {
+ return $array1 ?: $array2;
}
$result = (array) current($args);
@@ -653,39 +646,37 @@ class Set {
public static function normalize($list, $assoc = true, $sep = ',', $trim = true) {
if (is_string($list)) {
$list = explode($sep, $list);
- if ($trim) {
- foreach ($list as $key => $value) {
- $list[$key] = trim($value);
- }
- }
- if ($assoc) {
- return static::normalize($list);
- }
- } elseif (is_array($list)) {
- $keys = array_keys($list);
- $count = count($keys);
- $numeric = true;
-
- if (!$assoc) {
- for ($i = 0; $i < $count; $i++) {
- if (!is_int($keys[$i])) {
- $numeric = false;
- break;
- }
+ $list = ($trim) ? array_map('trim', $list) : $list;
+ return ($assoc) ? static::normalize($list) : $list;
+ }
+
+ if (!is_array($list)) {
+ return $list;
+ }
+
+ $keys = array_keys($list);
+ $count = count($keys);
+ $numeric = true;
+
+ if (!$assoc) {
+ for ($i = 0; $i < $count; $i++) {
+ if (!is_int($keys[$i])) {
+ $numeric = false;
+ break;
}
}
+ }
- if (!$numeric || $assoc) {
- $newList = array();
- for ($i = 0; $i < $count; $i++) {
- if (is_int($keys[$i]) && is_scalar($list[$keys[$i]])) {
- $newList[$list[$keys[$i]]] = null;
- } else {
- $newList[$keys[$i]] = $list[$keys[$i]];
- }
+ if (!$numeric || $assoc) {
+ $newList = array();
+ for ($i = 0; $i < $count; $i++) {
+ if (is_int($keys[$i]) && is_scalar($list[$keys[$i]])) {
+ $newList[$list[$keys[$i]]] = null;
+ } else {
+ $newList[$keys[$i]] = $list[$keys[$i]];
}
- $list = $newList;
}
+ $list = $newList;
}
return $list;
}
@@ -749,10 +740,9 @@ class Set {
$extract = static::extract($data, $path);
$result = $flatten($flatten, $extract);
- list($keys, $values) = array(
- static::extract($result, '/id'),
- static::extract($result, '/value')
- );
+ $keys = static::extract($result, '/id');
+ $values = static::extract($result, '/value');
+
$dir = ($dir === 'desc') ? SORT_DESC : SORT_ASC;
array_multisort($values, $dir, $keys, $dir);
$sorted = array();
@@ -763,126 +753,6 @@ class Set {
}
return $sorted;
}
-
- /**
- * Genric method for converting arrays and objects between different types
- *
- * @param string $type The type to convert to : array|object
- * @param string $data The array or object data to convert
- * @param string $options
- * @return void
- */
- public static function to($type, $data, array $options = array()) {
- if ($type === 'object') {
- return static::_toObject($data, $options);
- }
- return static::_toArray($data);
- }
-
- /**
- * Converts an object into an array. If `$object` is no object, reverse
- * will return the same value.
- *
- * @param object $object Object to make into an array.
- * @return array
- */
- public static function _toArray($object) {
- $out = array();
- if (is_object($object)) {
- $keys = get_object_vars($object);
- if (isset($keys['_name_'])) {
- $identity = $keys['_name_'];
- unset($keys['_name_']);
- }
- $new = array();
- foreach ($keys as $key => $value) {
- if (is_array($value)) {
- $new[$key] = static::_toArray($value);
- } else {
- if (isset($value->_name_)) {
- $new = array_merge($new, static::_toArray($value));
- } else {
- $new[$key] = static::_toArray($value);
- }
- }
- }
- if (isset($identity)) {
- $out[$identity] = $new;
- } else {
- $out = $new;
- }
- } elseif (is_array($object)) {
- foreach ($object as $key => $value) {
- $out[$key] = static::_toArray($value);
- }
- } else {
- $out = $object;
- }
- return $out;
- }
-
- /**
- * Maps the contents of the Set object to an object hierarchy. Maintains numeric
- * keys as arrays of objects.
- *
- * @param array $data The array.
- * @param string $options
- * @return object Hierarchical object.
- */
- public static function _toObject($data, array $options = array()) {
- if (empty($data)) {
- return $data;
- }
-
- $defaults = array('class' => '\stdClass', 'flatten' => true, 'name' => false);
- $options += $defaults;
- $out = new $options['class'];
- $name = $options['name'];
-
- if (is_array($data)) {
- $keys = array_keys($data);
- foreach ($data as $key => $value) {
- if (is_numeric($key)) {
- if (is_object($out)) {
- $out = get_object_vars($out);
- }
- $out[$key] = static::_toObject($value, $options);
- $isNamed = (
- !empty($options['name']) && $options['flatten'] === false &&
- is_object($out[$key]) && !isset($out[$key]->_name_) &&
- static::depth($value, true) >= 2
- );
- if ($isNamed) {
- $out[$key]->_name_ = $options['name'];
- }
- } elseif (is_array($value)) {
- if ($options['flatten'] === true) {
- $options['flatten'] = false;
- $out->_name_ = $key;
- foreach ($value as $key2 => $value2) {
- $out->{$key2} = static::_toObject($value2, array('flatten' => false));
- }
- } else {
- if (!is_numeric($key)) {
- $out->{$key} = static::_toObject($value, array(
- 'flatten' => false, 'name' => $key
- ));
- if (is_object($out->{$key}) && !isset($out->{$key}->_name_)) {
- $out->{$key}->_name_ = $key;
- }
- } else {
- $out->{$key} = static::_toObject($value, array('flatten' => true));
- }
- }
- } else {
- $out->{$key} = $value;
- }
- }
- } else {
- $out = $data;
- }
- return $out;
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/util/String.php b/libraries/lithium/util/String.php
index 323e391..2df4a60 100644
--- a/libraries/lithium/util/String.php
+++ b/libraries/lithium/util/String.php
@@ -2,15 +2,15 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* Copyright 2009, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @license http://opensource.org/licenses/mit-license.php The MIT License
*/
namespace lithium\util;
-use \Closure;
-use \Exception;
+use Closure;
+use Exception;
/**
* String manipulation utility class. Includes functionality for hashing, UUID generation,
@@ -18,66 +18,406 @@ use \Exception;
*
*/
class String {
+ /**
+ * UUID related constants
+ */
+ const clearVer = 15; // 00001111 Clears all bits of version byte
+ const version4 = 64; // 01000000 Sets the version bit
+ const clearVar = 63; // 00111111 Clears relevant bits of variant byte
+ const varRFC = 128; // 10000000 The RFC 4122 variant
/**
- * Generates a random UUID.
+ * A file pointer towards urandom if available, else false
*
- * @param mixed $context Used to determine the values for `'SERVER_ADDR'`, `'HOST'`
- * and `'HOSTNAME'`. Either a closure which is passed the requested context values, an
- * object with properties for each value or an array keyed by requested context value.
- * @return string An RFC 4122-compliant UUID.
- * @link http://www.ietf.org/rfc/rfc4122.txt
+ * @var resource|false
*/
- public static function uuid($context) {
- $val = function($value) use ($context) {
- switch (true) {
- case is_object($context) && is_callable($context):
- $result = $context($value);
- break;
- case is_object($context):
- $result = isset($context->$value) ? $context->$value : null;
- break;
- case is_array($context):
- $result = isset($context[$value]) ? $context[$value] : null;
- break;
+ protected static $_urandom;
+
+ /**
+ * Seeds the random generator if it has yet to be done.
+ *
+ * @return boolean Success.
+ */
+ public static function seed() {
+ // Seeding more than once means less entropy, not more, so bail
+ if (isset(static::$_urandom)) {
+ return false;
+ }
+
+ // Use urandom if the device is available
+ if (is_readable('/dev/urandom')) {
+ static::$_urandom = fopen('/dev/urandom', 'rb');
+ // Else seed PHP's mt_rand()
+ } else {
+ $seed = function() {
+ list($usec, $sec) = explode(' ', microtime());
+ $seed = (float) $sec + ((float) $usec * 100000);
+ if (function_exists('getmypid')) {
+ $seed .= getmypid();
+ }
+ return $seed;
+ };
+ mt_srand($seed());
+ static::$_urandom = false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Generates random bytes for use in UUIDs and password salts.
+ *
+ * The method seeds the random source automatically. It uses
+ * /dev/urandom if the latter is available; `md_rand()` if not.
+ *
+ * It can also be used to generate arbitrary bits:
+ *
+ * {{{
+ * $bits = bin2hex(String::random(8)); // 64 bits
+ * }}}
+ *
+ * @param integer $bytes The number of bytes to generate
+ * @param string Random bytes
+ */
+ public static function random($bytes) {
+ if (!isset(static::$_urandom)) {
+ static::seed();
+ }
+
+ if (static::$_urandom) {
+ $rand = fread(static::$_urandom, $bytes);
+ } else {
+ $rand = '';
+ for ($i = 0; $i < $bytes; $i++) {
+ $rand .= chr(mt_rand(0, 255));
}
- return $result;
- };
-
- $node = static::_hostname($val);
- $pid = function_exists('zend_thread_id') ? zend_thread_id() : getmypid();
- $pid = (!$pid || $pid > 65535) ? mt_rand(0, 0xfff) | 0x4000 : $pid;
- list($timeMid, $timeLow) = explode(' ', microtime());
-
- return sprintf(
- "%08x-%04x-%04x-%02x%02x-%04x%08x",
- (integer) $timeLow, (integer) substr($timeMid, 2) & 0xffff, mt_rand(0, 0xfff) | 0x4000,
- mt_rand(0, 0x3f) | 0x80, mt_rand(0, 0xff), $pid, $node
+ }
+
+ return $rand;
+ }
+
+ /**
+ * Generates an RFC 4122-compliant version 4 UUID.
+ *
+ * @return string The string representation of an RFC 4122-compliant, version 4 UUID.
+ * @link http://www.ietf.org/rfc/rfc4122.txt
+ */
+ public static function uuid() {
+ $uuid = static::random(16);
+
+ // Set version
+ $uuid[6] = chr(ord($uuid[6]) & static::clearVer | static::version4);
+
+ // Set variant
+ $uuid[8] = chr(ord($uuid[8]) & static::clearVar | static::varRFC);
+
+ // Return the uuid's string representation
+ return bin2hex(substr($uuid, 0, 4)) . '-'
+ . bin2hex(substr($uuid, 4, 2)) . '-'
+ . bin2hex(substr($uuid, 6, 2)) . '-'
+ . bin2hex(substr($uuid, 8, 2)) . '-'
+ . bin2hex(substr($uuid, 10, 6));
+ }
+
+ /**
+ * Uses PHP's hashing functions to create a hash of the string provided, using the options
+ * specified. The default hash algorithm is SHA-512.
+ *
+ * @link http://php.net/manual/en/function.hash.php PHP Manual: hash()
+ * @link http://php.net/manual/en/function.hash-hmac.php PHP Manual: hash_hmac()
+ * @link http://php.net/manual/en/function.hash-algos.php PHP Manual: hash_algos()
+ * @param string $string The string to hash.
+ * @param array $options Supported options:
+ * - `'type'` _string_: Any valid hashing algorithm. See the `hash_algos()` function to
+ * determine which are available on your system.
+ * - `'salt'` _string_: A _salt_ value which, if specified, will be prepended to the
+ * string.
+ * - `'key'` _string_: If specified `hash_hmac()` will be used to hash the string,
+ * instead of `hash()`, with `'key'` being used as the message key.
+ * - `'raw'` _boolean_: If `true`, outputs the raw binary result of the hash operation.
+ * Defaults to `false`.
+ * @return string Returns a hashed string.
+ */
+ public static function hash($string, array $options = array()) {
+ $defaults = array(
+ 'type' => 'sha512',
+ 'salt' => false,
+ 'key' => false,
+ 'raw' => false,
);
+ $options += $defaults;
+
+ if ($options['salt']) {
+ $string = $options['salt'] . $string;
+ }
+
+ if ($options['key']) {
+ return hash_hmac($options['type'], $string, $options['key'], $options['raw']);
+ }
+ return hash($options['type'], $string, $options['raw']);
}
/**
- * Create a hash from string using given method. Fallback on next available method.
+ * Hashes a password using PHP's `crypt()` and an optional salt. If no
+ * salt is supplied, a cryptographically strong salt will be generated
+ * using `String::genSalt()`.
+ *
+ * Using this function is the proper way to hash a password. Using naive
+ * methods such as `String::hash()` is fine to check a file's integrity,
+ * but fundamentally insecure for passwords, due to the invariable lack
+ * of a cryptographically strong salt.
+ *
+ * Moreover, `String::hashPassword()`'s cryptographically strong salts
+ * ensure that:
+ *
+ * - Two identical passwords will not be hashed the same way.
+ * - `String::genSalt()`'s count interator can later be increased (assuming
+ * BF or XDES is available) within Lithium or your application, without
+ * invalidating existing password hashes.
+ *
+ * Usage:
+ *
+ * {{{
+ * // Hash a password before storing it:
+ * $hashed = String::hashPassword($password);
+ *
+ * // Check a password by comparing it to its hashed value:
+ * $check = String::checkPassword($password, $hashed);
*
- * @param string $string String to hash.
- * @param string $type Method to use (sha1/sha256/md5, or any method supported
- * by the `hash()` function).
- * @param string $salt
- * @return string Hash.
+ * // Use a stronger custom salt:
+ * $salt = String::genSalt('bf', 16); // 2^16 iterations
+ * $hashed = String::hashPassword($password, $salt); // Very slow
+ * $check = String::checkPassword($password, $hashed); // Very slow
+ *
+ * // Forward/backward compatibility
+ * $salt1 = String::genSalt('bf', 6);
+ * $salt2 = String::genSalt('bf', 12);
+ * $hashed1 = String::hashPassword($password, $salt1); // Fast
+ * $hashed2 = String::hashPassword($password, $salt2); // Slow
+ * $check1 = String::checkPassword($password, $hashed1); // True
+ * $check2 = String::checkPassword($password, $hashed2); // True
+ * }}}
+ *
+ * @see lithium\util\String::genSalt()
+ * @param string $password The password to hash.
+ * @param string $salt Optional. The salt string.
+ * @return string The hashed password.
+ * The result's length will be:
+ * - 60 chars for Blowfish hashes
+ * - 20 chars for XDES hashes
+ * - 34 chars for MD5 hashes
+ **/
+ public static function hashPassword($password, $salt = null) {
+ return crypt($password, $salt ?: static::genSalt());
+ }
+
+ /**
+ * Compares a password and its hashed value using PHP's `crypt()`.
+ *
+ * @see lithium\util\String::hashPassword()
+ * @see lithium\util\String::genSalt()
+ * @param string $password The password to check
+ * @param string $hash The hashed password to compare
+ * @return boolean Whether the password is correct or not
*/
- public static function hash($string, $type = null, $salt = null) {
- $string = $salt . $string;
+ public static function checkPassword($password, $hash) {
+ return $hash == crypt($password, $hash);
+ }
+ /**
+ * Generates a cryptographically strong salt, using the best available
+ * method (tries Blowfish, then XDES, and fallbacks to MD5), for use in
+ * `String::hashPassword()`.
+ *
+ * Blowfish and XDES are adaptive hashing algorithms. MD5 is not. Adaptive
+ * hashing algorithms are designed in such a way that when computers get
+ * faster, you can tune the algorithm to be slower by increasing the number
+ * of hash iterations, without introducing incompatibility with existing
+ * passwords.
+ *
+ * To pick an appropriate iteration count for adaptive algorithms, consider
+ * that the original DES crypt was designed to have the speed of 4 hashes
+ * per second on the hardware of that time. Slower than 4 hashes per second
+ * would probably dampen usability. Faster than 100 hashes per second is
+ * probably too fast. The defaults generate about 10 hashes per second
+ * using a dual-core 2.2GHz CPU.
+ *
+ * Note1: this salt generator is different from naive salt implementations
+ * (e.g. `md5(microtime())`) that are invariably found in OSS PHP applications,
+ * in that it uses all of the available bits of entropy for the supplied salt
+ * method.
+ *
+ * Note2: this method should not be used as custom salts, for instance in a
+ * custom password hasher. Indeed, salts are prefixed with information expected
+ * by PHP's `crypt()`. To get an arbitrarily long, cryptographically strong salt
+ * consisting in random sequences of alpha numeric characters, combine
+ * `String::random()` and `String::encode64()` instead.
+ *
+ * @link http://php.net/manual/en/function.crypt.php
+ * @link http://www.postgresql.org/docs/9.0/static/pgcrypto.html
+ * @see lithium\util\String::hashPassword()
+ * @param string $type The hash type. Optional. Defaults to '`bf`'.
+ * Supported values include:
+ * - `'bf'`: Blowfish (128 salt bits, adaptive, max 72 chars)
+ * - `'xdes'`: XDES (24 salt bits, adaptive, max 8 chars)
+ * - `'md5'`: MD5 (48 salt bits, non-adaptive, unlimited length)
+ * @param integer $count Optional. The base-2 logarithm of the iteration
+ * count, for adaptive algorithms. Defaults to:
+ * - `10` for Blowfish
+ * - `18` for XDES
+ * @return string The salt string.
+ */
+ public static function genSalt($type = null, $count = null) {
switch (true) {
- case (($type == 'sha1' || !$type) && function_exists('sha1')):
- return sha1($string);
- case ($type == 'sha256' && function_exists('mhash')):
- return bin2hex(mhash(MHASH_SHA256, $string));
- case (function_exists('hash')):
- return hash($type, $string);
+ case CRYPT_BLOWFISH == 1 && (!$type || $type === 'bf'):
+ return static::_genSaltBF($count);
+ case CRYPT_EXT_DES == 1 && (!$type || $type === 'xdes'):
+ return static::_genSaltXDES($count);
default:
+ return static::_genSaltMD5();
+ }
+ }
+
+ /**
+ * Encodes bytes into an `./0-9A-Za-z` alphabet, for use as salt when
+ * hashing passwords.
+ *
+ * Note: this is not the same as RFC 1421, or `base64_encode()`, which
+ * uses an `+/0-9A-Za-z` alphabet.
+ *
+ * This function can be combined with `String::random()` to generate random
+ * sequences of `./0-9A-Za-z` characters:
+ *
+ * {{{
+ * $salt = String::encode64(String::random(8)); // 64 bits
+ * }}}
+ *
+ * @see lithium\util\String::random()
+ * @param string $input The input bytes.
+ * @return string The same bytes in the `/.0-9A-Za-z` alphabet.
+ */
+ public static function encode64($input) {
+ $base64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+ $i = 0;
+
+ $count = strlen($input);
+ $output = '';
+
+ do {
+ $value = ord($input[$i++]);
+ $output .= $base64[$value & 0x3f];
+
+ if ($i < $count) {
+ $value |= ord($input[$i]) << 8;
+ }
+ $output .= $base64[($value >> 6) & 0x3f];
+
+ if ($i++ >= $count) {
+ break;
+ }
+ if ($i < $count) {
+ $value |= ord($input[$i]) << 16;
+ }
+ $output .= $base64[($value >> 12) & 0x3f];
+
+ if ($i++ >= $count) {
+ break;
+ }
+ $output .= $base64[($value >> 18) & 0x3f];
+ } while ($i < $count);
+
+ return $output;
+ }
+
+ /**
+ * Generates a Blowfish salt for use in `String::hashPassword()`.
+ *
+ * @param integer $count The base-2 logarithm of the iteration count.
+ * Defaults to `10`. Can be `4` to `31`.
+ * @return string $salt
+ */
+ protected static function _genSaltBf($count = 10) {
+ $count = (integer) $count;
+ if ($count < 4 || $count > 31) {
+ $count = 10;
}
- return md5($string);
+
+ // We don't use the encode64() method here because it could result
+ // in 2 bits less of entropy depending on the last char.
+ $base64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ $i = 0;
+
+ $input = static::random(16); // 128 bits of salt
+ $output = '';
+
+ do {
+ $c1 = ord($input[$i++]);
+ $output .= $base64[$c1 >> 2];
+ $c1 = ($c1 & 0x03) << 4;
+ if ($i >= 16) {
+ $output .= $base64[$c1];
+ break;
+ }
+
+ $c2 = ord($input[$i++]);
+ $c1 |= $c2 >> 4;
+ $output .= $base64[$c1];
+ $c1 = ($c2 & 0x0f) << 2;
+
+ $c2 = ord($input[$i++]);
+ $c1 |= $c2 >> 6;
+ $output .= $base64[$c1];
+ $output .= $base64[$c2 & 0x3f];
+ } while (1);
+
+ return '$2a$'
+ // zeroize $count
+ . chr(ord('0') + $count / 10) . chr(ord('0') + $count % 10)
+ . '$' . $output;
+ }
+
+ /**
+ * Generates an Extended DES salt for use in `String::hashPassword()`.
+ *
+ * @param integer $count The base-2 logarithm of the iteration count.
+ * Defaults to `18`. Can be `1` to `24`. 1 will be stripped
+ * from the non-log value, e.g. 2^18 - 1, to ensure we don't
+ * use a weak DES key.
+ * @return string The XDES salt.
+ */
+ protected static function _genSaltXDES($count = 18) {
+ $count = (integer) $count;
+ if ($count < 1 || $count > 24) {
+ $count = 16;
+ }
+
+ // Count should be odd to not reveal weak DES keys
+ $count = (1 << $count) - 1;
+
+ $base64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+ $output = '_'
+ // iterations
+ . $base64[$count & 0x3f]
+ . $base64[($count >> 6) & 0x3f]
+ . $base64[($count >> 12) & 0x3f]
+ . $base64[($count >> 18) & 0x3f]
+ // 24 bits of salt
+ . static::encode64(static::random(3));
+
+ return $output;
+ }
+
+ /**
+ * Generates an MD5 salt for use in `String::hashPassword()`.
+ *
+ * @return string The MD5 salt.
+ */
+ protected static function _genSaltMD5() {
+ $output = '$1$'
+ // 48 bits of salt
+ . static::encode64(static::random(6));
+ return $output;
}
/**
@@ -98,8 +438,8 @@ class String {
* @param string $options Available options are:
* - `'after'`: The character or string after the name of the variable place-holder
* (defaults to `null`).
- * - `'before'`: The character or string in front of the name of the variable place-holder
- * (defaults to `':'`).
+ * - `'before'`: The character or string in front of the name of the variable
+ * place-holder (defaults to `':'`).
* - `'clean'`: A boolean or array with instructions for `String::clean()`.
* - `'escape'`: The character or string used to escape the before character or string
* (defaults to `'\'`).
@@ -115,7 +455,7 @@ class String {
'after' => '}',
'escape' => null,
'format' => null,
- 'clean' => false
+ 'clean' => false,
);
$options += $defaults;
$format = $options['format'];
@@ -329,46 +669,6 @@ class String {
}
return empty($results) ? array() : array_map('trim', $results);
}
-
- /**
- * Used by `String::uuid()` to get the hostname from request context data. Uses fallbacks to get
- * the current host name or IP, depending on what values are available.
- *
- * @param mixed $context An array (i.e. `$_SERVER`), `Request` object, or anonymous function
- * containing host data.
- * @return string Returns the host name or IP for use in generating a UUID.
- */
- protected static function _hostname($context) {
- $node = $context('SERVER_ADDR');
-
- if (strpos($node, ':') !== false) {
- if (substr_count($node, '::')) {
- $pad = str_repeat(':0000', 8 - substr_count($node, ':'));
- $node = str_replace('::', $pad . ':', $node);
- }
- $node = explode(':', $node);
- $ipv6 = '';
-
- foreach ($node as $id) {
- $ipv6 .= str_pad(base_convert($id, 16, 2), 16, 0, STR_PAD_LEFT);
- }
- $node = base_convert($ipv6, 2, 10);
- $node = (strlen($node) < 38) ? null : crc32($node);
- } elseif (empty($node)) {
- $host = $context('HOSTNAME');
- $host = $host ?: $context('HOST');
-
- if (!empty($host)) {
- $ip = gethostbyname($host);
- $node = ($ip === $host) ? crc32($host) : $node = ip2long($ip);
- }
- } elseif ($node !== '127.0.0.1') {
- $node = ip2long($node);
- } else {
- $node = crc32(rand());
- }
- return $node;
- }
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/util/Validator.php b/libraries/lithium/util/Validator.php
index 36feb10..3a32235 100644
--- a/libraries/lithium/util/Validator.php
+++ b/libraries/lithium/util/Validator.php
@@ -2,14 +2,14 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
namespace lithium\util;
-use \lithium\util\Set;
-use \InvalidArgumentException;
+use lithium\util\Set;
+use InvalidArgumentException;
/**
* The `Validator` class provies static access to commonly used data validation logic. These common
@@ -17,44 +17,113 @@ use \InvalidArgumentException;
* codes, but also include general checks for regular expressions and booleans and numericality.
*
* General data checking is done by using `Validator` statically. Rules can be specified as a
- * parameter to the `rule()` method or automatically accessed via the `is[RuleName]()` method name
+ * parameter to the `rule()` method or accessed directly via the `is[RuleName]()` method name
* convention:
*
* {{{
- * use \lithium\util\Validator;
+ * use lithium\util\Validator;
*
* // The following are equivalent:
* Validator::rule('email', 'foo@example.com'); // true
* Validator::isEmail('foo-at-example.com'); // false
* }}}
*
- * Data can also be validated against multiple rules, each having its own associated error
+ * Data can also be validated against multiple rules, each having their own associated error
* message. The rule structure is array-based and hierarchical based on rule names and
- * messages. Resposes match the keys present in `$data` up with an array of rules which they
- * violate.
+ * messages. Resposes match the keys present in the `$data` parameter of `check()` up with an array
+ * of rules which they violate.
*
- * {{{
- * $rules = array(
- * 'title' => 'please enter a title',
- * 'email' => array(
- * array('notEmpty', 'message' => 'email is empty'),
- * array('email', 'message' => 'email is not valid'),
- * )
- * );
- * $data = array('email' => 'foo');
- * Validator::check($data, $rules);
+ * {{{ embed:lithium\tests\cases\util\ValidatorTest::testCheckMultipleHasFirstError(1-15) }}}
*
- * // result:
+ * See the `check()` method for more information an multi-value datasets. Custom validation rules
+ * can also be added to `Validator` at runtime. These can either take the form of regular expression
+ * strings or functions supplied to the `add()` method.
*
- * array(
- * 'title' => array('please enter a title'),
- * 'email' => array('email is not valid')
- * );
+ * ### Rules
*
- * }}}
+ * The `Validator` class includes a series of commonly-used rules by default, any of which may be
+ * used in calls to `rule()` or `check()`, or called directly as a method. Additionally, many rules
+ * have a variety of different _formats_ in which they may be specified. The following is the list
+ * of the built-in rules, but keep in mind that none of them are hard-coded. Any rule may be
+ * overridden by adding a new rule of the same name using the `add()` method.
+ *
+ * - `notEmpty`: Checks that a string contains at least one non-whitespace character.
+ *
+ * - `alphaNumeric`: Checks that a string contains only integer or letters.
+ *
+ * - `lengthBetween`: Checks that a string length is within a specified range. Spaces are included
+ * in the character count. The available options are `'min'` and `'max'`, which designate the
+ * minimum and maximum length of the string.
+ *
+ * - `blank`: Checks that a field is left blank **OR** only whitespace characters are present in its
+ * value. Whitespace characters include spaces, tabs, carriage returns and newlines.
+ *
+ * - `creditCard`: Checks that a value is a valid credit card number. This rule is divided into a
+ * series of formats: `'amex'`, `'bankcard'`, `'diners'`, `'disc'`, `'electron'`, `'enroute'`,
+ * `'jcb'`, `'maestro'`, `'mc'`, `'solo'`, `'switch'`, `'visa'`, `'voyager'`, `'fast'`. If no
+ * format value is specified, the value defaults to `'any'`, which will validate the value if
+ * _any_ of the available formats match. You can also use the `'fast'` format, which does a
+ * high-speed, low-fidelity check to ensure that the value looks like a real credit card number.
+ * This rule includes one option, `'deep'`, which (if set to `true`) validates the value using the
+ * [Luhn algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm) if the format validation is
+ * successful. See the `luhn` validator below for more details.
+ *
+ * - `date`: Checks that a value is a valid date that complies with one or more formats. Also
+ * validates leap years. Possible formats are `'dmy'` (27-12-2010 or 27-12-10 separators can be a
+ * space, period, dash, forward slash), `'mdy'` (12-27-2010 or 12-27-10 separators can be a space,
+ * period, dash, forward slash), `'ymd'` (2010-12-27 or 10-12-27 separators can be a space,
+ * period, dash, forward slash), `'dMy'` (27 December 2010 or 27 Dec 2010), `'Mdy'` (December 27,
+ * 2010 or Dec 27, 2010 comma is optional), `'My'` (December 2010 or Dec 2010) or `'my'` (12/2010
+ * separators can be a space, period, dash, forward slash).
+ *
+ * - `time`: Checks that a value is a valid time. Validates time as 24hr (HH:MM) or am/pm
+ * ([ H]H:MM[a|p]m). Does not allow / validate seconds.
+ *
+ * - `boolean`: Checks that a value is a boolean integer or `true` or `false`.
+ *
+ * - `decimal`: Checks that a value is a valid decimal. Takes one option, `'precision'`, which is
+ * an optional integer value defining the level of precision the decimal number must match.
+ *
+ * - `email`: Checks that a value is (probably) a valid email address. The subject of validating
+ * an actual email address is complicated and problematic. A regular expression that correctly
+ * validates addresses against [RFC 5322](http://tools.ietf.org/html/rfc5322) would be several
+ * pages long, with the drawback of being unable to keep up as new top-level domains are added.
+ * Instead, this validator uses PHP's internal input filtering API to check the format, and
+ * provides an option, `'deep'` ( _boolean_) which, if set to `true`, will validate that the email
+ * address' domain contains a valid MX record. Keep in mind, this is just one of the many ways to
+ * validate an email address in the overall context of an application. For other ideas or
+ * examples, [ask Sean](http://seancoates.com/).
+ *
+ * - `ip`: Validates a string as a valid IPv4 or IPv6 address.
+ *
+ * - `money`: Checks that a value is a valid monetary amount. This rule has two formats, `'right'`
+ * and `'left'`, which indicates which side the monetary symbol (i.e. $) appears on.
+ *
+ * - `numeric`: Checks that a value is numeric.
+ *
+ * - `phone`: Check that a value is a valid phone number, non-locale-specific phone number.
+ *
+ * - `postalCode`: Checks that a given value is a valid US postal code.
*
- * Custom validation rules can also be added to `Validator` at runtime. These can either take the
- * form of regular expression strings or functions supplied to the `add()` method.
+ * - `inRange`: Checks that a numeric value is within a specified range. This value has two options,
+ * `'upper'` and `'lower'`, which specify the boundary of the value.
+ *
+ * - `url`: Checks that a value is a valid URL according to
+ * [RFC 2395](http://www.faqs.org/rfcs/rfc2396.html). Uses PHP's filter API, and accepts any
+ * options accepted for
+ * [the validation URL filter](http://www.php.net/manual/en/filter.filters.validate.php).
+ *
+ * - `luhn`: Checks that a value is a valid credit card number according to the
+ * [Luhn algorithm](http://en.wikipedia.org/wiki/Luhn_algorithm). (See also: the `creditCard`
+ * validator).
+ *
+ * - `inList`: Checks that a value is in a pre-defined list of values. This validator accepts one
+ * option, `'list'`, which is an array containing acceptable values.
+ *
+ * - `regex`: Checks that a value appears to be a /-delimited regular expression, possibly
+ * containing PCRE-compatible options flags.
+ *
+ * - `uuid`: Checks that a value is a valid UUID.
*/
class Validator extends \lithium\core\StaticObject {
@@ -71,11 +140,13 @@ class Validator extends \lithium\core\StaticObject {
protected static $_rules = array();
/**
- * Default options used when defining a new validator rule.
+ * Default options used when defining a new validator rule. Each key contains method-specific
+ * options that should always be applied, or options that should be applied to all rules in the
+ * `'defaults'` key.
*
- * @var array Options
* @see lithium\util\Validator::add()
* @see lithium\util\Validator::rule()
+ * @var array
*/
protected static $_options = array(
'defaults' => array('contains' => true)
@@ -111,7 +182,7 @@ class Validator extends \lithium\core\StaticObject {
'visa' => '/^4\\d{12}(\\d{3})?$/',
'voyager' => '/^8699[0-9]{11}$/',
'fast' => '/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|3' .
- '(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})$/'
+ '(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})$/',
),
'date' => array(
'dmy' => '%^(?:(?:31(\\/|-|\\.|\\x20)(?:0?[13578]|1[02]))\\1|(?:(?:29|30)' .
@@ -153,15 +224,13 @@ class Validator extends \lithium\core\StaticObject {
),
'ip' => function($value, $format = null, array $options = array()) {
$options += array('flags' => array());
- return (boolean) filter_var(
- $value, FILTER_VALIDATE_IP, array('flags' => $options['flags'])
- );
+ return (boolean) filter_var($value, FILTER_VALIDATE_IP, $options);
},
'money' => array(
'right' => '/^(?!0,?\d)(?:\d{1,3}(?:([, .])\d{3})?(?:\1\d{3})*|(?:\d+))' .
'((?!\1)[,.]\d{2})?(?<!\x{00a2})\p{Sc}?$/u',
'left' => '/^(?!\x{00a2})\p{Sc}?(?!0,?\d)(?:\d{1,3}(?:([, .])\d{3})?' .
- '(?:\1\d{3})*|(?:\d+))((?!\1)[,.]\d{2})?$/u'
+ '(?:\1\d{3})*|(?:\d+))((?!\1)[,.]\d{2})?$/u',
),
'notEmpty' => '/[^\s]+/m',
'phone' => '/^\+?[0-9\(\)\-]{10,20}$/',
@@ -172,7 +241,6 @@ class Validator extends \lithium\core\StaticObject {
'boolean' => function($value) {
$bool = is_bool($value);
$filter = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
-
return ($bool || $filter !== null);
},
'decimal' => function($value, $format = null, array $options = array()) {
@@ -183,7 +251,7 @@ class Validator extends \lithium\core\StaticObject {
return false;
}
}
- return (boolean) filter_var($value, FILTER_VALIDATE_FLOAT);
+ return (filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE) !== null);
},
'inList' => function($value, $format, $options) {
$options += array('list' => array());
@@ -230,25 +298,23 @@ class Validator extends \lithium\core\StaticObject {
}
return is_finite($value);
},
- 'uuid' => "/{$alnum}{8}-{$alnum}{4}-{$alnum}{4}-{$alnum}{4}-{$alnum}{12}/",
+ 'uuid' => "/^{$alnum}{8}-{$alnum}{4}-{$alnum}{4}-{$alnum}{4}-{$alnum}{12}$/",
'email' => function($value) {
return filter_var($value, FILTER_VALIDATE_EMAIL);
},
'url' => function($value, $format = null, array $options = array()) {
$options += array('flags' => array());
- return (boolean) filter_var(
- $value, FILTER_VALIDATE_URL, array('flags' => $options['flags'])
- );
+ return (boolean) filter_var($value, FILTER_VALIDATE_URL, $options);
}
);
- $emptyCheck = function($self, $params, $chain) {
+ $isEmpty = function($self, $params, $chain) {
extract($params);
return (empty($value) && $value != '0') ? false : $chain->next($self, $params, $chain);
};
- static::$_methodFilters[$class]['alphaNumeric'] = array($emptyCheck);
- static::$_methodFilters[$class]['notEmpty'] = array($emptyCheck);
+ static::$_methodFilters[$class]['alphaNumeric'] = array($isEmpty);
+ static::$_methodFilters[$class]['notEmpty'] = array($isEmpty);
static::$_methodFilters[$class]['creditCard'] = array(function($self, $params, $chain) {
extract($params);
@@ -263,26 +329,24 @@ class Validator extends \lithium\core\StaticObject {
return $options['deep'] ? Validator::isLuhn($value) : true;
});
- static::$_methodFilters[$class]['email'] = array(
- function($self, $params, $chain) {
- extract($params);
- $defaults = array('deep' => false);
- $options += $defaults;
-
- if (!$chain->next($self, $params, $chain)) {
- return false;
- }
- if (!$options['deep']) {
- return true;
- }
- list($prefix, $host) = explode('@', $params['value']);
+ static::$_methodFilters[$class]['email'] = array(function($self, $params, $chain) {
+ extract($params);
+ $defaults = array('deep' => false);
+ $options += $defaults;
- if (getmxrr($host, $mxhosts)) {
- return is_array($mxhosts);
- }
+ if (!$chain->next($self, $params, $chain)) {
return false;
}
- );
+ if (!$options['deep']) {
+ return true;
+ }
+ list($prefix, $host) = explode('@', $params['value']);
+
+ if (getmxrr($host, $mxhosts)) {
+ return is_array($mxhosts);
+ }
+ return false;
+ });
}
/**
@@ -305,35 +369,84 @@ class Validator extends \lithium\core\StaticObject {
}
/**
- * Checks a set of values against a specified rules list.
- *
- * This method may be used to validate any arbitrary array data against a set of validation
- * rules.
- *
- * @param object $values An array of key/value pairs, where the values are to be checked.
- * @param string $rules array of rules to check against object properties
- * @return mixed When all validation rules pass
+ * Checks a set of values against a specified rules list. This method may be used to validate
+ * any arbitrary array of data against a set of validation rules.
+ *
+ * @param array $values An array of key/value pairs, where the values are to be checked.
+ * @param array $rules An array of rules to check the values in `$values` against. Each key in
+ * `$rules` should match a key contained in `$values`, and each value should be a
+ * validation rule in one of the allowable formats. For example, if you are
+ * validating a data set containing a `'credit_card'` key, possible values for
+ * `$rules` would be as follows:
+ * - `array('credit_card' => 'You must include a credit card number')`: This is the
+ * simplest form of validation rule, in which the value is simply a message to
+ * display if the rule fails. Using this format, all other validation settings
+ * inherit from the defaults, including the validation rule itself, which only
+ * checks to see that the corresponding key in `$values` is present and contains
+ * a value that is not empty. _Please note when globalizing validation messages:_
+ * When specifying messages, it may be preferable to use a code string (i.e.
+ * `'ERR_NO_TITLE'`) instead of the full text of the validation error. These code
+ * strings may then be translated by the appropriate tools in the templating
+ * layer.
+ * - `array('credit_card' => array('creditCard', 'message' => 'Invalid CC #'))`:
+ * In the second format, the validation rule (in this case `creditCard`) and
+ * associated configuration are specified as an array, where the rule to use is
+ * the first value in the array (no key), and additional settings are specified
+ * as other keys in the array. Please see the list below for more information on
+ * allowed keys.
+ * - The final format allows you to apply multiple validation rules to a single
+ * value, and it is specified as follows:
+ *
+ * `array('credit_card' => array(
+ * array('notEmpty', 'message' => 'You must include credit card number'),
+ * array('creditCard', 'message' => 'Your credit card number must be valid')
+ * ));`
+ * @param array $options Validator-specific options.
+ *
+ * Each rule defined as an array can contain any of the following settings (in addition to the
+ * first value, which represents the rule to be used):
+ * - `'message'` _string_: The error message to be returned if the validation rule fails. See
+ * the note above regarding globalization of error messages.
+ * - `'required`' _boolean_: Represents whether the value is required to be present in
+ * `$values`. If `'required'` is set to `false`, the validation rule will be skipped if the
+ * corresponding key is not present. Defaults to `true`.
+ * - `'skipEmpty'` _boolean_: Similar to `'required'`, this setting (if `true`) will cause the
+ * validation rule to be skipped if the corresponding value is empty (an empty string or
+ * `null`). Defaults to `false`.
+ * - `'format'` _string_: If the validation rule has multiple format definitions (see the
+ * `add()` or `__init()` methods), the name of the format to be used can be specified here.
+ * Additionally, two special values can be used: either `'any'`, which means that all formats
+ * will be checked and the rule will pass if any format passes, or `'all'`, which requires
+ * all formats to pass in order for the rule check to succeed.
+ * @return array Returns an array containing all validation failures for data in `$values`,
+ * where each key matches a key in `$values`, and each value is an array of that
+ * element's validation errors.
*/
- public static function check($values, $rules) {
+ public static function check(array $values, array $rules, array $options = array()) {
$defaults = array(
'notEmpty',
'message' => null,
'required' => true,
'skipEmpty' => false,
'format' => 'any',
- 'last' => false
+ 'on' => null,
);
$errors = array();
+ $events = (array) (isset($options['events']) ? $options['events'] : null);
foreach ($rules as $field => $rules) {
$rules = is_string($rules) ? array('message' => $rules) : $rules;
$rules = is_array(current($rules)) ? $rules : array($rules);
$errors[$field] = array();
+ $options['field'] = $field;
foreach ($rules as $key => $rule) {
- $rule += $defaults;
+ $rule += $defaults + compact('values');
list($name) = $rule;
+ if ($events && $rule['on'] && !array_intersect($events, (array) $rule['on'])) {
+ continue;
+ }
if (!isset($values[$field])) {
if ($rule['required']) {
$errors[$field][] = $rule['message'] ?: $key;
@@ -343,7 +456,7 @@ class Validator extends \lithium\core\StaticObject {
if (empty($values[$field]) && $rule['skipEmpty']) {
continue;
}
- if (!static::rule($name, $values[$field], $rule['format'], $rule)) {
+ if (!static::rule($name, $values[$field], $rule['format'], $rule + $options)) {
$errors[$field][] = $rule['message'] ?: $key;
}
}
@@ -389,12 +502,13 @@ class Validator extends \lithium\core\StaticObject {
* - `$value` _mixed_: This is the actual value to be validated (as in the above example).
* - `$format` _string_: Often, validation rules come in multiple "formats", for example:
* postal codes, which vary by country or region. Defining multiple formats allows you to
- * retian flexibility in how you validate data. In cases where a user's country of origin is
- * known, the appropriate validation rule may be selected. In cases where it is not known,
- * the value of `$format` may be `'any'`, which should pass if any format matches. In cases
- * where validation rule formats are not mutually exclusive, the value may be `'all'`, in
- * which case all must match.
- * - `$options` _array_: This parameter allows a validation rule to implement custom options.
+ * retian flexibility in how you validate data. In cases where a user's country of origin
+ * is known, the appropriate validation rule may be selected. In cases where it is not
+ * known, the value of `$format` may be `'any'`, which should pass if any format matches.
+ * In cases where validation rule formats are not mutually exclusive, the value may be
+ * `'all'`, in which case all must match.
+ * - `$options` _array_: This parameter allows a validation rule to implement custom
+ * options.
*
* @see lithium\util\Validator::$_rules
* @param mixed $name The name of the validation rule (string), or an array of key/value pairs
@@ -427,12 +541,12 @@ class Validator extends \lithium\core\StaticObject {
* @param mixed $value
* @param string $format
* @param string $options
- * @return boolean
- * @todo Write tests for pre- and post-filtering
+ * @return boolean Returns `true` or `false` indicating whether the validation rule check
+ * succeeded or failed.
*/
public static function rule($rule, $value, $format = 'any', array $options = array()) {
if (!isset(static::$_rules[$rule])) {
- throw new InvalidArgumentException("Rule '{$rule}' is not a validation rule");
+ throw new InvalidArgumentException("Rule `{$rule}` is not a validation rule.");
}
$defaults = isset(static::$_options[$rule]) ? static::$_options[$rule] : array();
$options = (array) $options + $defaults + static::$_options['defaults'];
@@ -513,232 +627,6 @@ class Validator extends \lithium\core\StaticObject {
return $options['all'];
};
}
-
- /**
- * Checks that a string contains something other than whitespace
- *
- * Returns true if string contains something other than whitespace
- *
- * $value can be passed as an array:
- * array('check' => 'valueToCheck');
- *
- * @param mixed $value Value to check
- * @return boolean Success
- */
- // public static function isNotEmpty($value) {}
-
- /**
- * Checks that a string contains only integer or letters
- *
- * Returns true if string contains only integer or letters
- *
- * $value can be passed as an array:
- * array('check' => 'valueToCheck');
- *
- * @param mixed $value Value to check
- * @return boolean Success
- */
- // public static function isAlphaNumeric($value) {}
-
- /**
- * Checks that a string length is within s specified range.
- * Spaces are included in the character count.
- * Returns true is string matches value min, max, or between min and max,
- *
- * @param string $value Value to check for length
- * @param integer $min Minimum value in range (inclusive)
- * @param integer $max Maximum value in range (inclusive)
- * @return boolean Success
- */
- // public static function isLengthBetween($value, $min, $max) {}
-
- /**
- * Returns true if field is left blank **OR** only whitespace characters are present in its
- * value. Whitespace characters include spaces, tabs, carriage returns and newlines.
- *
- * $value can be passed as an array:
- * array('check' => 'valueToCheck');
- *
- * @param mixed $value Value to check
- * @return boolean Success
- */
- // public static function isBlank($value) {}
-
- /**
- * Validates credit card numbers. Returns true if `$value` is in the proper credit card format.
- *
- * @see lithium\util\Validator::isLuhn()
- * @param mixed $value credit card number to validate
- * @param mixed $type 'all' may be passed as a sting, defaults to fast which checks format of
- * most major credit cards if an array is used only the values of the array
- * are checked. Example: array('amex', 'bankcard', 'maestro')
- * @param boolean $deep set to true this will check the Luhn algorithm of the credit card.
- * @return boolean Success
- */
- // public static function isCreditCard($value, $format = 'fast', $deep = false) {}
-
- /**
- * Date validation, determines if the string passed is a valid date.
- * keys that expect full month, day and year will validate leap years
- *
- * @param string $value a valid date string
- * @param mixed $format Use a string or an array of the keys below. Arrays should be passed
- * as array('dmy', 'mdy', etc). Possible values are:
- * - dmy 27-12-2006 or 27-12-06 separators can be a space, period, dash, forward slash
- * - mdy 12-27-2006 or 12-27-06 separators can be a space, period, dash, forward slash
- * - ymd 2006-12-27 or 06-12-27 separators can be a space, period, dash, forward slash
- * - dMy 27 December 2006 or 27 Dec 2006
- * - Mdy December 27, 2006 or Dec 27, 2006 comma is optional
- * - My December 2006 or Dec 2006
- * - my 12/2006 separators can be a space, period, dash, forward slash
- * @return boolean Success
- */
- // public static function date($value, $format = 'ymd') {}
-
- /**
- * Time validation, determines if the string passed is a valid time.
- * Validates time as 24hr (HH:MM) or am/pm ([H]H:MM[a|p]m)
- * Does not allow/validate seconds.
- *
- * @param string $value a valid time string
- * @return boolean Success
- */
- // public static function time($value) {}
-
- /**
- * Boolean validation, determines if value passed is a boolean integer or true/false.
- *
- * @param string $value a valid boolean
- * @return boolean Success
- */
- // public static function isBoolean($value) {}
-
- /**
- * Checks that a value is a valid decimal. If $places is null, the $value is allowed to be a
- * scientific float. If no decimal point is found a false will be returned. Both the sign
- * and exponent are optional.
- *
- * @param integer $value The value the test for decimal
- * @param integer $precision if set $value value must have exactly $places after the decimal
- * point
- * @return boolean Success
- */
- // public static function isDecimal($value, $format = null) {}
-
- /**
- * Validates for an email address.
- *
- * @param string $value Value to check
- * @param boolean $deep Perform a deeper validation (if true), by also checking availability
- * of host
- * @return boolean Success
- */
- // public static function isEmail($value, $deep = false) {}
-
- /**
- * Validates IPv4 addresses.
- *
- * @param string $value The string to test.
- * @return boolean Success
- */
- // public static function isIp($value) {}
-
- /**
- * Checks that a value is a monetary amount.
- *
- * @param string $value Value to check
- * @param string $format Where symbol is located (left/right)
- * @return boolean Success
- */
- // public static function isMoney($value, $format = 'left') {}
-
- /**
- * Checks if a value is numeric.
- *
- * @param string $value Value to check
- * @return boolean Success
- */
- // public static function isNumeric($value) {}
-
- /**
- * Check that a value is a valid phone number.
- *
- * @param mixed $value Value to check (string or array)
- * @param string $regex Regular expression to use
- * @param string $country Country code (defaults to 'all')
- * @return boolean Success
- */
- //public static function isPhone($value, $format = 'any') {}
-
- /**
- * Checks that a given value is a valid postal code.
- *
- * @param mixed $value Value to check
- * @param string $regex Regular expression to use
- * @param string $country Country to use for formatting
- * @return boolean Success
- */
- // public static function isPostalCode($value, $country = null) {}
-
- /**
- * Validate that a number is in specified range.
- * if $lower and $upper are not set, will return true if
- * $value is a legal finite on this platform
- *
- * @param string $value Value to check
- * @param integer $lower Lower limit
- * @param integer $upper Upper limit
- * @return boolean Success
- */
- // public static function isInRange($value, $lower = null, $upper = null) {}
-
- /**
- * Checks that a value is a valid Social Security Number.
- *
- * @param mixed $value Value to check
- * @param string $regex Regular expression to use
- * @param string $country Country
- * @return boolean Success
- */
- // public static function isSsn($value, $format = null) {}
-
- /**
- * Checks that a value is a valid URL according to
- * http://www.w3.org/Addressing/URL/url-spec.txt
- *
- * The regex checks for the following component parts:
- * - A valid, optional, scheme
- * - A valid ip address OR
- * - A valid domain name as defined by section 2.3.1 of
- * http://www.ietf.org/rfc/rfc1035.txt with an optional port number
- * - An optional valid path
- * - An optional query string (get parameters)
- * - An optional fragment (anchor tag)
- *
- * @param string $value Value to check
- * @return boolean Success
- */
- // public static function url($value, $strict = false) {}
-
- /**
- * Luhn algorithm
- *
- * Checks that a credit card number is a valid Luhn sequence.
- *
- * @param mixed $value A string or integer representing a credit card number.
- * @link http://en.wikipedia.org/wiki/Luhn_algorithm
- * @return boolean Success
- */
- // public static function isLuhn($value) {}
-
- /**
- * Checks if a value is in a given list.
- *
- * @param string $value Value to check
- * @param array $list List to check against
- * @return boolean Success
- */
- // public static function isInList($value, $list) {}
}
?>
\ No newline at end of file
diff --git a/libraries/lithium/util/collection/Filters.php b/libraries/lithium/util/collection/Filters.php
index 5f80b20..acb596c 100644
--- a/libraries/lithium/util/collection/Filters.php
+++ b/libraries/lithium/util/collection/Filters.php
@@ -2,7 +2,7 @@
/**
* Lithium: the most rad php framework
*
- * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org)
+ * @copyright Copyright 2011, Union of RAD (http://union-of-rad.org)
* @license http://opensource.org/licenses/bsd-license.php The BSD License
*/
@@ -19,7 +19,7 @@ namespace lithium\util\collection;
* of filters, which are applied in these methods and passed to `Filters::run()`.
*
* When implementing a custom filter system outside of Lithium, you can create your own list of
- * filters, and pass it to `$options['items']` in the `run()` method.
+ * filters, and pass it to `$options['data']` in the `run()` method.
*
* When creating a filter to apply to a method, you need the name of the method you want to call,
* along with a **closure**, that defines what you want the filter to do. All filters take the same
@@ -44,7 +44,7 @@ namespace lithium\util\collection;
* Within the framework, you can call `applyFilter()` on any object (static or instantiated) and
* pass the name of the method you would like to filter, along with the filter itself. For example:
*
- * {{{use \lithium\action\Dispatcher;
+ * {{{use lithium\action\Dispatcher;
*
* Dispatcher::applyFilter('run', function($self, $params, $chain) {
* // Custom pre-dispatch logic goes here
@@ -68,13 +68,82 @@ namespace lithium\util\collection;
*/
class Filters extends \lithium\util\Collection {
- protected $_autoConfig = array('items', 'class', 'method');
+ /**
+ * An array of filters indexed by class and method name, stored so that they can be lazily
+ * applied to classes which are not loaded yet.
+ *
+ * @var array
+ */
+ protected static $_lazyFilters = array();
+ /**
+ * This is the list of configuration settings that will be automatically applied to the
+ * properties of each `Filters` instance.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array('data', 'class', 'method');
+
+ /**
+ * The fully-namespaced class name of the class containing the method being filtered.
+ *
+ * @see lithium\util\collection\Filters::method()
+ * @var string
+ */
protected $_class = null;
+ /**
+ * The name of the method being filtered by the current instance of `Filters`.
+ *
+ * @see lithium\util\collection\Filters::method()
+ * @var string
+ */
protected $_method = null;
/**
+ * Lazily applies a filter to a method of a static class.
+ *
+ * This method is useful if you want to apply a filter inside a global bootstrap file to a
+ * static class which may or may not be loaded during every request, or which may be loaded
+ * lazily elsewhere in your application. If the class is already loaded, the filter will be
+ * applied immediately.
+ *
+ * However, if the class has not been loaded, the filter will be stored and applied to the class
+ * the first time the method specified in `$method` is called. This works for any class which
+ * extends `StaticObject`.
+ *
+ * @see lithium\core\StaticObject
+ * @param string $class The fully namespaced name of a **static** class to which the filter will
+ * be applied. The class name specified in `$class` **must** extend
+ * `StaticObject`, or else statically implement the `applyFilter()` method.
+ * @param string $method The method to which the filter will be applied.
+ * @param closure $filter The filter to apply to the class method.
+ * @return void
+ */
+ public static function apply($class, $method, $filter) {
+ if (class_exists($class, false)) {
+ return $class::applyFilter($method, $filter);
+ }
+ static::$_lazyFilters[$class][$method][] = $filter;
+ }
+
+ /**
+ * Checks to see if the given class / method has any filters which have been applied lazily,
+ * and not yet attached.
+ *
+ * If a filter has been lazily applied (using `Filters::apply()`) to a class which is/was not
+ * yet loaded, checks to see if the filter is still being held, or has been applied. The filter
+ * will not be applied until the method being filtered has been called.
+ *
+ * @see lithium\util\collection\Filters::apply()
+ * @param string $class Fully-namespaced class name.
+ * @param string $method Method name.
+ */
+ public static function hasApplied($class, $method) {
+ return isset(static::$_lazyFilters[$class][$method]);
+ }
+
+ /**
* Collects a set of filters to iterate. Creates a filter chain for the given class/method,
* executes it, and returns the value.
*
@@ -88,34 +157,48 @@ class Filters extends \lithium\util\Collection {
*
* -'class': The name of the class that initiated the filter chain.
* -'method': The name of the method that initiated the filter chain.
- * -'items': An array of callable objects (usually closures) to be iterated through.
- * By default, execution will be nested such that the first item will be executed
- * first, and will be the last to return.
- * @return Returns the value returned by the first closure in `$options['items`]`.
+ * -`'data'` _array_: An array of callable objects (usually closures) to be iterated
+ * through. By default, execution will be nested such that the first item will be
+ * executed first, and will be the last to return.
+ * @return Returns the value returned by the first closure in `$options['data`]`.
*/
public static function run($class, $params, array $options = array()) {
- $defaults = array('class' => null, 'method' => null, 'items' => array());
- $chain = new Filters((array) $options + $defaults);
- return $chain->rewind()->__invoke($class, $params, $chain);
+ $defaults = array('class' => null, 'method' => null, 'data' => array());
+ $options += $defaults;
+ $lazyFilterCheck = (is_string($class) && $options['method']);
+
+ if (($lazyFilterCheck) && isset(static::$_lazyFilters[$class][$options['method']])) {
+ $filters = static::$_lazyFilters[$class][$options['method']];
+ unset(static::$_lazyFilters[$class][$options['method']]);
+ $options['data'] = array_merge($filters, $options['data']);
+
+ foreach ($filters as $filter) {
+ $class::applyFilter($options['method'], $filter);
+ }
+ }
+
+ $chain = new Filters($options);
+ $next = $chain->rewind();
+ return $next($class, $params, $chain);
}
/**
* Provides short-hand convenience syntax for filter chaining.
*
+ * @see lithium\core\Object::applyFilter()
+ * @see lithium\core\Object::_filter()
* @param object $self The object instance that owns the filtered method.
* @param array $params An associative array containing the parameters passed to the filtered
* method.
* @param array $chain The Filters object instance containing this chain of filters.
* @return mixed Returns the return value of the next filter in the chain.
- * @see lithium\core\Object::applyFilter()
- * @see lithium\core\Object::_filter()
- * @todo Implement checks allowing params to be null, to traverse filter chain
*/
public function next($self, $params, $chain) {
if (empty($self) || empty($chain)) {
return parent::next();
}
- return parent::next()->__invoke($self, $params, $chain);
+ $next = parent::next();
+ return $next($self, $params, $chain);
}
/**